Compare commits

...

133 Commits

Author SHA1 Message Date
wangruidong
2ec9a43317 fix: Any change to the LDAP server URI should require re-authentication and explicit re-entry of
the bind password, not reuse stored credentials
2025-10-23 15:29:47 +08:00
wangruidong
06be56ef06 fix: Enhance state check to include query parameter for session validation 2025-10-23 14:41:50 +08:00
ibuler
b2a618b206 perf: user sugguestion limit and serializer 2025-10-23 14:40:37 +08:00
wangruidong
1039c2e320 perf: ws/ldap perms check 2025-10-23 14:26:24 +08:00
fit2bot
8d7267400d fix: OpenID Only allow existing users to log in operate log error (#16013)
Co-authored-by: wangruidong <940853815@qq.com>
2025-10-22 14:53:12 +08:00
ibuler
d67e473884 perf: add auto cleanup branches 2025-10-22 11:46:09 +08:00
fit2bot
70068c9253 perf: Reduce the number of pub sub processing threads (#16072)
* perf: Reduce the number of pub sub processing threads

* perf: Using thread pool to process messages

---------

Co-authored-by: wangruidong <940853815@qq.com>
2025-10-21 17:41:14 +08:00
wangruidong
d68babb2e1 fix: Using winrm protocol to transfer files did not create a directory problem 2025-10-21 17:31:41 +08:00
wangruidong
afb6f466d5 perf: AppletHost translate 2025-10-21 17:31:03 +08:00
ibuler
453ad331ee perf: token retrieve 2025-10-21 10:48:08 +08:00
feng
d309d11a8f perf: Command count 2025-10-16 17:11:42 +08:00
feng
4771693a56 fix: dashboard command count 2025-10-16 16:25:01 +08:00
Chenyang Shen
cefc820ac1 Merge pull request #16163 from jumpserver/pr@dev@asset_acl_filter
perf: Asset acl filter action
2025-10-16 15:25:38 +08:00
feng
d007afdb43 perf: Asset acl filter action 2025-10-16 15:21:32 +08:00
feng
e8921a43be perf: Translate 2025-10-16 14:32:59 +08:00
wangruidong
a9b44103d4 fix: Handle email sending failure with appropriate error response 2025-10-16 11:28:41 +08:00
jiangweidong
4abf2bded6 perf: oracle cdb mode, common users need to start username with C## 2025-10-16 09:57:54 +08:00
feng
54693089a0 perf: replace command objects 2025-10-15 19:32:14 +08:00
Aaron3S
0b859dd502 feat: update i18n 2025-10-15 19:17:44 +08:00
feng
3fb27f969a perf: datamaskingrule perm 2025-10-15 17:33:27 +08:00
Aaron3S
45627a1d92 feat: update data masking rule filter 2025-10-15 16:51:58 +08:00
feng
245e2dab66 perf: Filter effective 2025-10-15 16:51:32 +08:00
Aaron3S
8f0a41b1a8 fix: fix data masking org problem 2025-10-15 15:51:14 +08:00
feng
1a9e56c520 perf: Translate 2025-10-15 15:24:19 +08:00
feng
67c2f471b4 perf: oracle sqlserver db2 dameng clickhouse redis db_name allow_blank 2025-10-15 11:30:00 +08:00
github-actions[bot]
b04f96f5f2 perf: Update Dockerfile with new base image tag 2025-10-14 18:09:25 +08:00
Eric
30f03b7d89 perf: change python base
perf: update deps
2025-10-14 18:09:25 +08:00
wangruidong
28a97d0b5a fix: Incorrect language display in some email content 2025-10-14 18:08:21 +08:00
Eric
3410686690 perf: fix python base ci 2025-10-14 17:47:31 +08:00
Eric
6860e2327f perf: add python base ci build 2025-10-14 17:41:05 +08:00
feng
20253e760c perf: translate 2025-10-14 17:13:42 +08:00
Aaron3S
a63cfde8d2 feat: add translate 2025-10-14 16:03:38 +08:00
feng
92e250e03b perf: user_can_authenticate add logger 2025-10-14 15:48:47 +08:00
wangruidong
098f0950cb fix: Incorrect language display in email content 2025-10-14 15:33:04 +08:00
feng
39b0830a6b perf: web script default [] 2025-10-14 13:59:11 +08:00
wangruidong
2e847bc2bc fix: Error in updating message subscription 500 2025-10-14 10:14:50 +08:00
wangruidong
f82f31876a fix: Mysql has set a gateway, and the command execution failed. 2025-10-14 10:14:23 +08:00
github-actions[bot]
cde182c015 perf: Update Dockerfile with new base image tag 2025-10-10 17:06:14 +08:00
Eric
b990cdf561 perf: update deps 2025-10-10 17:06:14 +08:00
feng
c9a062823d perf: Translate 2025-10-10 17:02:30 +08:00
feng
643ba4fc15 fix: Asset web script dont create 2025-10-10 11:43:11 +08:00
feng
d16a55bbe2 perf: Ticket details cannot view assets from other organizations. 2025-10-09 18:41:25 +08:00
fit2bot
ae31554729 perf: AppletHostOnly label match (#16109)
Co-authored-by: wangruidong <940853815@qq.com>
2025-10-09 18:13:37 +08:00
github-actions[bot]
53b47980a2 perf: Update Dockerfile with new base image tag 2025-10-09 16:55:50 +08:00
Eric
d31b5ee570 perf: update Dockerfile-base 2025-10-09 16:55:50 +08:00
feng
65aea1ea36 perf: Push account and change secret support gid 2025-10-09 16:39:32 +08:00
feng
5abb5c5d5a perf: Themes deep blue 2025-10-09 15:36:14 +08:00
feng
93e41a5004 perf: Luna themes default 2025-10-09 15:02:37 +08:00
feng
95f51bbe48 perf: Perference add themes 2025-10-09 14:47:11 +08:00
feng
0184d292ec perf: MFA code 2025-10-09 14:29:08 +08:00
fit2bot
23a6d320c7 feat: update i18n (#16101)
* feat: data masking

* feat: update i18n

---------

Co-authored-by: Aaron3S <chenyang@fit2cloud.com>
Co-authored-by: 老广 <ibuler@qq.com>
2025-10-09 10:03:11 +08:00
Aaron3S
b16304c48a feat: data masking 2025-10-09 09:59:23 +08:00
Gerry.tan
7cd1e4d3a0 perf: Dynamically configure the validity period of the email verification code 2025-09-28 11:26:32 +08:00
Eric
64a9987c3f perf: update rdp params 2025-09-28 11:20:52 +08:00
feng
18bfe312fa perf: open web ui 2025-09-25 15:49:10 +08:00
wangruidong
c593f91d77 fix: Account backup: when sending to the mailbox fails, the task status also shows the success problem. 2025-09-18 15:44:35 +08:00
feng
46da05652a fix: Fixed the issue where the final connection verification failed when the domain name contains . 2025-09-18 14:08:00 +08:00
feng
9249aba1a9 perf: Video player version 2025-09-18 11:03:58 +08:00
fit2bot
eca637c120 perf: Translate msg template (#16050)
* fix: Correct translation for device and user limits in django.po

* perf: Translate msg template

---------

Co-authored-by: wangruidong <940853815@qq.com>
2025-09-17 19:04:06 +08:00
feng
ddacd5fce1 fix: Ticket direct approval 2025-09-17 18:58:16 +08:00
wangruidong
3ca5c04099 fix: Add ignore_https_errors option to browser context 2025-09-17 16:30:54 +08:00
wangruidong
6603a073ec fix: Case 2025-09-17 15:32:23 +08:00
wangruidong
d745f7495a fix: Conflict 2025-09-17 15:32:23 +08:00
wangruidong
76f1667c89 perf: Restore msg template default value config 2025-09-17 15:32:23 +08:00
wangruidong
1ab1954299 fix: reset password msg error 2025-09-17 15:32:23 +08:00
wangruidong
c8335999a4 perf: Translate msg template 2025-09-17 15:32:23 +08:00
feng
5b4a67362d perf: Translate 2025-09-17 15:10:54 +08:00
fit2bot
e025073da2 fix: The number of exported data is incorrect (#16043)
Co-authored-by: wangruidong <940853815@qq.com>
2025-09-16 18:52:24 +08:00
feng
2155bc6862 perf: Migrate 2025-09-16 16:46:30 +08:00
wangruidong
953b515817 perf: Add is_alive filter to TerminalFilterSet 2025-09-16 16:30:57 +08:00
ibuler
7f7a354b2d fix: get obj error on queryset limit 2025-09-16 16:28:54 +08:00
Eric
2b2f7ea3f0 perf: add rdp true color 24 bit 2025-09-16 16:28:14 +08:00
feng
529123e1b5 perf: Translate 2025-09-16 16:15:09 +08:00
ibuler
e156ab6ad8 fix: force page limit 2025-09-16 13:48:06 +08:00
wangruidong
3c1fd134ae fix: There is something wrong with the format of the site message 2025-09-16 13:33:43 +08:00
Bai
b15f663c87 fix: AK/SK remained valid after the user expired. 2025-09-16 13:32:25 +08:00
wangruidong
93906dff0a fix: Export report pdf failed 2025-09-16 11:36:42 +08:00
Bai
307befdacd fix: login acl action reject > reviewers 500 2025-09-16 11:17:42 +08:00
feng626
dbfc4d3981 Revert "perf: User acl 500"
This reverts commit 849edd33c1.
2025-09-16 11:15:51 +08:00
feng
849edd33c1 perf: User acl 500 2025-09-16 10:50:41 +08:00
feng
37cceec8fe perf: get protocols error 500 2025-09-16 10:40:42 +08:00
feng
d2494c25cc perf: Translate 2025-09-15 19:19:01 +08:00
feng
023952582e fix: Push account failed 2025-09-15 15:32:27 +08:00
halo
863fe95100 perf: client version 2025-09-12 18:53:16 +08:00
wangruidong
4b0bdb18c9 perf: Template msg example error 2025-09-12 18:47:47 +08:00
Eric
10da053a95 perf: change applet-hosts view default limit 2025-09-12 18:43:38 +08:00
mikebofs
c40bc46520 fix: asset permission exclude accounts with -action 2025-09-12 11:16:27 +08:00
feng
a732cc614e perf: Asset user login notify 2025-09-11 14:16:00 +08:00
ibuler
bb29d519c6 perf: exclude accounts date expired 2025-09-11 11:42:44 +08:00
ibuler
b56c3a76a7 fix: user option error 2025-09-11 11:21:59 +08:00
fit2bot
ab908d24a7 perf: add i18n (#16001)
* perf: change some api view default limit

* perf: add i18n

---------

Co-authored-by: mikebofs <mikebofs@gmail.com>
2025-09-10 18:18:18 +08:00
fit2bot
79cabe1b3c feat: setting email template content (#15974)
* feat: setting email template content

* perf: tempale list

* perf: custom template render to string

* perf: content serialize valid

* perf: Custom msg template base class

* perf: Template content reset

* perf: Update templates config

* perf: Remove useless code

---------

Co-authored-by: wangruidong <940853815@qq.com>
2025-09-10 16:49:52 +08:00
feng
231b7287c1 perf: Notify info css optimization 2025-09-10 14:04:19 +08:00
feng
be7a4c0d6e perf: Create account unique message 2025-09-09 17:39:18 +08:00
feng
009da19050 perf: Change secret windows password cannot contain > ^ 2025-09-09 16:41:45 +08:00
feng
dfda6b1e08 perf: Change secret del over report 2025-09-09 15:48:03 +08:00
fit2bot
59b40578d8 fix: adhoc SQL Server 2008 (#15984)
* fix: Resolve the issue of errors occurring during automated execution with SQL Server 2008

* fix: adhoc SQL Server 2008

* perf: add todo information

---------

Co-authored-by: halo <wuyihuangw@gmail.com>
2025-09-09 14:26:42 +08:00
Eric
e5db28c014 perf: user add has_public_keys 2025-09-09 14:23:39 +08:00
Eric
6d1f26b0f8 perf: add redis cluster mode setting 2025-09-09 13:51:53 +08:00
Ewall555
2333dbbe33 fix: avoid AttributeError when default_limit is missing 2025-09-09 13:32:52 +08:00
fit2bot
16461b0fa9 perf: support global search (#15961)
* perf: support global search

* perf: change serach

* perf: search model add asset permission

---------

Co-authored-by: mikebofs <mikebofs@gmail.com>
Co-authored-by: ibuler <ibuler@qq.com>
2025-09-05 16:40:18 +08:00
mikebofs
528b0ea1ba perf: change some api view default limit 2025-09-05 16:20:26 +08:00
ibuler
60f06adaa9 fix: wechat or phone decrypt err 2025-09-04 11:59:04 +08:00
Bai
7a6187b95f fix: temp token backend 2025-09-03 18:10:10 +08:00
Bai
aacaf3a174 perf: aks encrypt 2025-09-03 11:16:04 +08:00
Bai
3c9d2534fa perf: aks encrypt 2025-09-03 11:16:04 +08:00
wangruidong
4f79abe678 perf: Connect methods acl allow accept action 2025-09-03 11:00:56 +08:00
fit2bot
ae9956ff91 chore: change readme 2025-09-02 15:22:44 +08:00
Bai
429677e0ce perf: readme 2025-09-02 14:54:28 +08:00
ibuler
034ee65157 perf: decrypt secret logic 2025-09-02 10:38:10 +08:00
Eric
fdd7d9b6b1 perf: add vnc client method 2025-09-02 10:34:39 +08:00
wangruidong
db0e21f5d9 fix: Lazy import Azure and Google Cloud dependencies 2025-08-29 11:10:43 +08:00
wangruidong
468b84eb3d perf: Validate connection token id 2025-08-29 11:09:40 +08:00
ibuler
28d5475d0f perf: try to decrypt then origin value 2025-08-29 11:00:02 +08:00
ibuler
b9c60d856f perf: allow some api page no limits 2025-08-28 17:05:11 +08:00
feng
bd1d73c6dd perf: Report localtime 2025-08-28 15:39:54 +08:00
wangruidong
bf92c756d4 fix: Ensure command arguments are safely quoted in safe_run_cmd 2025-08-28 14:14:55 +08:00
feng
62ebe0d636 perf: Third login redirect url query string 2025-08-27 14:45:56 +08:00
github-actions[bot]
0b1fea8492 perf: Update Dockerfile with new base image tag 2025-08-27 11:05:19 +08:00
mikebofs
65b5f573f8 perf: change requirements 2025-08-27 11:05:19 +08:00
mikebofs
bb639e1fe7 perf: revert django-simple-history version 2025-08-27 10:43:21 +08:00
fit2bot
395b868dcf perf: swagger done (#15865)
* perf: swagger upgrade

* perf: upgrade to drf-spectacular

* perf: 添加部分注解

* perf: swagger done

---------

Co-authored-by: ibuler <ibuler@qq.com>
2025-08-27 10:27:01 +08:00
wangruidong
1350b774b3 perf: Improve chart rendering wait logic in export process 2025-08-26 16:20:22 +08:00
wrd
af7a00c1b1 fix: typo 2025-08-26 15:31:13 +08:00
wangruidong
965ec7007c perf: Enhance eager loading by including labels in queryset 2025-08-26 15:31:13 +08:00
fit2bot
1372fd7535 feat: asset permission support exclude some account
* perf: add perm exclude

* perf: exclude node action account

* perf: add i18n

* perf: pop exclude account

---------

Co-authored-by: mikebofs <mikebofs@gmail.com>
2025-08-26 14:57:57 +08:00
wangruidong
3b0ef4cca7 fix: Add nmap to Dockerfile dependencies 2025-08-25 16:29:10 +08:00
Aaron3S
6832abdaad feat: change some translate 2025-08-25 11:05:49 +08:00
feng
c6bf290dbb perf: Report translate 2025-08-22 18:57:14 +08:00
feng
23ab66c11a perf: Translate 2025-08-22 18:05:30 +08:00
feng
1debaa5547 perf: report perm 2025-08-22 17:53:52 +08:00
Bai
47413966c9 perf: captcha > CAPTCHA 2025-08-22 16:25:45 +08:00
Eric
703f39607c perf: default allow hosts 2025-08-22 14:12:45 +08:00
256 changed files with 29686 additions and 4663 deletions

View File

@@ -0,0 +1,46 @@
name: Build and Push Python Base Image
on:
workflow_dispatch:
inputs:
tag:
description: 'Tag to build'
required: true
default: '3.11-slim-bullseye-v1'
type: string
jobs:
build-and-push:
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
image: tonistiigi/binfmt:qemu-v7.0.0-28
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract repository name
id: repo
run: echo "REPO=$(basename ${{ github.repository }})" >> $GITHUB_ENV
- name: Build and push multi-arch image
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
push: true
file: Dockerfile-python
tags: jumpserver/core-base:python-${{ inputs.tag }}

123
.github/workflows/cleanup-branches.yml vendored Normal file
View File

@@ -0,0 +1,123 @@
name: Cleanup PR Branches
on:
schedule:
# 每天凌晨2点运行
- cron: '0 2 * * *'
workflow_dispatch:
# 允许手动触发
inputs:
dry_run:
description: 'Dry run mode (default: true)'
required: false
default: 'true'
type: boolean
jobs:
cleanup-branches:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # 获取所有分支和提交历史
- name: Setup Git
run: |
git config --global user.name "GitHub Actions"
git config --global user.email "actions@github.com"
- name: Get dry run setting
id: dry-run
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "dry_run=${{ github.event.inputs.dry_run }}" >> $GITHUB_OUTPUT
else
echo "dry_run=true" >> $GITHUB_OUTPUT
fi
- name: Cleanup branches
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DRY_RUN: ${{ steps.dry-run.outputs.dry_run }}
run: |
echo "Starting branch cleanup..."
echo "Dry run mode: $DRY_RUN"
# 获取所有本地分支
git fetch --all --prune
# 获取以 pr 或 repr 开头的分支
branches=$(git branch -r | grep -E 'origin/(pr|repr)' | sed 's/origin\///' | grep -v 'HEAD')
echo "Found branches matching pattern:"
echo "$branches"
deleted_count=0
skipped_count=0
for branch in $branches; do
echo ""
echo "Processing branch: $branch"
# 检查分支是否有未合并的PR
pr_info=$(gh pr list --head "$branch" --state open --json number,title,state 2>/dev/null)
if [ $? -eq 0 ] && [ "$pr_info" != "[]" ]; then
echo " ⚠️ Branch has open PR(s), skipping deletion"
echo " PR info: $pr_info"
skipped_count=$((skipped_count + 1))
continue
fi
# 检查分支是否有已合并的PR可选如果PR已合并也可以删除
merged_pr_info=$(gh pr list --head "$branch" --state merged --json number,title,state 2>/dev/null)
if [ $? -eq 0 ] && [ "$merged_pr_info" != "[]" ]; then
echo " ✅ Branch has merged PR(s), safe to delete"
echo " Merged PR info: $merged_pr_info"
else
echo " No PRs found for this branch"
fi
# 执行删除操作
if [ "$DRY_RUN" = "true" ]; then
echo " 🔍 [DRY RUN] Would delete branch: $branch"
deleted_count=$((deleted_count + 1))
else
echo " 🗑️ Deleting branch: $branch"
# 删除远程分支
if git push origin --delete "$branch" 2>/dev/null; then
echo " ✅ Successfully deleted remote branch: $branch"
deleted_count=$((deleted_count + 1))
else
echo " ❌ Failed to delete remote branch: $branch"
fi
fi
done
echo ""
echo "=== Cleanup Summary ==="
echo "Branches processed: $(echo "$branches" | wc -l)"
echo "Branches deleted: $deleted_count"
echo "Branches skipped: $skipped_count"
if [ "$DRY_RUN" = "true" ]; then
echo ""
echo "🔍 This was a DRY RUN - no branches were actually deleted"
echo "To perform actual deletion, run this workflow manually with dry_run=false"
fi
- name: Create summary
if: always()
run: |
echo "## Branch Cleanup Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Workflow:** ${{ github.workflow }}" >> $GITHUB_STEP_SUMMARY
echo "**Run ID:** ${{ github.run_id }}" >> $GITHUB_STEP_SUMMARY
echo "**Dry Run:** ${{ steps.dry-run.outputs.dry_run }}" >> $GITHUB_STEP_SUMMARY
echo "**Triggered by:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Check the logs above for detailed information about processed branches." >> $GITHUB_STEP_SUMMARY

View File

@@ -1,11 +1,9 @@
name: 🔀 Sync mirror to Gitee
on:
push:
branches:
- master
- dev
create:
schedule:
# 每天凌晨3点运行
- cron: '0 3 * * *'
jobs:
mirror:
@@ -14,7 +12,6 @@ jobs:
steps:
- name: mirror
continue-on-error: true
if: github.event_name == 'push' || (github.event_name == 'create' && github.event.ref_type == 'tag')
uses: wearerequired/git-mirror-action@v1
env:
SSH_PRIVATE_KEY: ${{ secrets.GITEE_SSH_PRIVATE_KEY }}

View File

@@ -1,4 +1,4 @@
FROM jumpserver/core-base:20250819_064003 AS stage-build
FROM jumpserver/core-base:20251014_095903 AS stage-build
ARG VERSION
@@ -19,7 +19,7 @@ RUN set -ex \
&& python manage.py compilemessages
FROM python:3.11-slim-bullseye
FROM jumpserver/core-base:python-3.11-slim-bullseye-v1
ENV LANG=en_US.UTF-8 \
PATH=/opt/py3/bin:$PATH
@@ -33,6 +33,7 @@ ARG TOOLS=" \
default-libmysqlclient-dev \
openssh-client \
sshpass \
nmap \
bubblewrap"
ARG APT_MIRROR=http://deb.debian.org

View File

@@ -1,4 +1,4 @@
FROM python:3.11-slim-bullseye
FROM jumpserver/core-base:python-3.11-slim-bullseye-v1
ARG TARGETARCH
COPY --from=ghcr.io/astral-sh/uv:0.6.14 /uv /uvx /usr/local/bin/
# Install APT dependencies
@@ -28,7 +28,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
&& echo "no" | dpkg-reconfigure dash
# Install bin tools
ARG CHECK_VERSION=v1.0.4
ARG CHECK_VERSION=v1.0.5
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 \

11
Dockerfile-python Normal file
View File

@@ -0,0 +1,11 @@
FROM python:3.11-slim-bullseye
ARG TARGETARCH
# Install APT dependencies
ENV DEBIAN_FRONTEND=noninteractive
RUN set -eux; \
apt-get update; \
apt-get -y --no-install-recommends upgrade; \
rm -rf /var/lib/apt/lists/*
# upgrade pip and setuptools
RUN pip install --no-cache-dir --upgrade pip setuptools wheel

View File

@@ -2,7 +2,7 @@
<a name="readme-top"></a>
<a href="https://jumpserver.com" target="_blank"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a>
## An open-source PAM tool (Bastion Host)
## An open-source PAM platform (Bastion Host)
[![][license-shield]][license-link]
[![][docs-shield]][docs-link]
@@ -19,7 +19,7 @@
## What is JumpServer?
JumpServer is an open-source Privileged Access Management (PAM) tool that provides DevOps and IT teams with on-demand and secure access to SSH, RDP, Kubernetes, Database and RemoteApp endpoints through a web browser.
JumpServer is an open-source Privileged Access Management (PAM) platform that provides DevOps and IT teams with on-demand and secure access to SSH, RDP, Kubernetes, Database and RemoteApp endpoints through a web browser.
<picture>

View File

@@ -11,6 +11,7 @@ from accounts.const import ChangeSecretRecordStatusChoice
from accounts.filters import AccountFilterSet, NodeFilterBackend
from accounts.mixins import AccountRecordViewLogMixin
from accounts.models import Account, ChangeSecretRecord
from assets.const.gpt import create_or_update_chatx_resources
from assets.models import Asset, Node
from authentication.permissions import UserConfirmation, ConfirmType
from common.api.mixin import ExtraFilterFieldsMixin
@@ -18,6 +19,7 @@ from common.drf.filters import AttrRulesFilterBackend
from common.permissions import IsValidUser
from common.utils import lazyproperty, get_logger
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.utils import tmp_to_root_org
from rbac.permissions import RBACPermission
logger = get_logger(__file__)
@@ -43,6 +45,7 @@ class AccountViewSet(OrgBulkModelViewSet):
'clear_secret': 'accounts.change_account',
'move_to_assets': 'accounts.delete_account',
'copy_to_assets': 'accounts.add_account',
'chat': 'accounts.view_account',
}
export_as_zip = True
@@ -152,6 +155,13 @@ class AccountViewSet(OrgBulkModelViewSet):
def copy_to_assets(self, request, *args, **kwargs):
return self._copy_or_move_to_assets(request, move=False)
@action(methods=['get'], detail=False, url_path='chat')
def chat(self, request, *args, **kwargs):
with tmp_to_root_org():
__, account = create_or_update_chatx_resources()
serializer = self.get_serializer(account)
return Response(serializer.data)
class AccountSecretsViewSet(AccountRecordViewLogMixin, AccountViewSet):
"""
@@ -190,6 +200,7 @@ class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, AccountRecordViewLogMixi
rbac_perms = {
'GET': 'accounts.view_accountsecret',
}
queryset = Account.history.model.objects.none()
@lazyproperty
def account(self) -> Account:

View File

@@ -12,6 +12,8 @@ class VirtualAccountViewSet(OrgBulkModelViewSet):
filterset_fields = ('alias',)
def get_queryset(self):
if getattr(self, "swagger_fake_view", False):
return VirtualAccount.objects.none()
return VirtualAccount.get_or_init_queryset()
def get_object(self, ):

View File

@@ -41,6 +41,7 @@ class AutomationAssetsListApi(generics.ListAPIView):
class AutomationRemoveAssetApi(generics.UpdateAPIView):
model = BaseAutomation
queryset = BaseAutomation.objects.all()
serializer_class = serializers.UpdateAssetSerializer
http_method_names = ['patch']
@@ -59,6 +60,7 @@ class AutomationRemoveAssetApi(generics.UpdateAPIView):
class AutomationAddAssetApi(generics.UpdateAPIView):
model = BaseAutomation
queryset = BaseAutomation.objects.all()
serializer_class = serializers.UpdateAssetSerializer
http_method_names = ['patch']

View File

@@ -154,12 +154,10 @@ class ChangSecretAddAssetApi(AutomationAddAssetApi):
model = ChangeSecretAutomation
serializer_class = serializers.ChangeSecretUpdateAssetSerializer
class ChangSecretNodeAddRemoveApi(AutomationNodeAddRemoveApi):
model = ChangeSecretAutomation
serializer_class = serializers.ChangeSecretUpdateNodeSerializer
class ChangeSecretStatusViewSet(OrgBulkModelViewSet):
perm_model = ChangeSecretAutomation
filterset_class = ChangeSecretStatusFilterSet

View File

@@ -62,7 +62,8 @@ class ChangeSecretDashboardApi(APIView):
status_counts = defaultdict(lambda: defaultdict(int))
for date_finished, status in results:
date_str = str(date_finished.date())
dt_local = timezone.localtime(date_finished)
date_str = str(dt_local.date())
if status == ChangeSecretRecordStatusChoice.failed:
status_counts[date_str]['failed'] += 1
elif status == ChangeSecretRecordStatusChoice.success:

View File

@@ -150,6 +150,9 @@ class CheckAccountEngineViewSet(JMSModelViewSet):
http_method_names = ['get', 'options']
def get_queryset(self):
if getattr(self, "swagger_fake_view", False):
return CheckAccountEngine.objects.none()
return CheckAccountEngine.get_default_engines()
def filter_queryset(self, queryset: list):

View File

@@ -63,12 +63,10 @@ class PushAccountRemoveAssetApi(AutomationRemoveAssetApi):
model = PushAccountAutomation
serializer_class = serializers.PushAccountUpdateAssetSerializer
class PushAccountAddAssetApi(AutomationAddAssetApi):
model = PushAccountAutomation
serializer_class = serializers.PushAccountUpdateAssetSerializer
class PushAccountNodeAddRemoveApi(AutomationNodeAddRemoveApi):
model = PushAccountAutomation
serializer_class = serializers.PushAccountUpdateNodeSerializer
serializer_class = serializers.PushAccountUpdateNodeSerializer

View File

@@ -235,8 +235,8 @@ class AccountBackupHandler:
except Exception as e:
error = str(e)
print(f'\033[31m>>> {error}\033[0m')
self.execution.status = Status.error
self.execution.summary['error'] = error
self.manager.status = Status.error
self.manager.summary['error'] = error
def backup_by_obj_storage(self):
object_id = self.execution.snapshot.get('id')

View File

@@ -113,6 +113,16 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
if host.get('error'):
return host
inventory_hosts = []
if asset.type == HostTypes.WINDOWS:
if self.secret_type == SecretType.SSH_KEY:
host['error'] = _("Windows does not support SSH key authentication")
return host
new_secret = self.get_secret(account)
if '>' in new_secret or '^' in new_secret:
host['error'] = _("Windows password cannot contain special characters like > ^")
return host
host['ssh_params'] = {}
accounts = self.get_accounts(account)
@@ -130,11 +140,6 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
if asset.type == HostTypes.WINDOWS:
accounts = accounts.filter(secret_type=SecretType.PASSWORD)
inventory_hosts = []
if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY:
print(f'Windows {asset} does not support ssh key push')
return inventory_hosts
for account in accounts:
h = deepcopy(host)
h['name'] += '(' + account.username + ')' # To distinguish different accounts

View File

@@ -5,12 +5,14 @@
tasks:
- name: Test SQLServer connection
community.general.mssql_script:
mssql_script:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: |
SELECT @@version
register: db_info
@@ -23,45 +25,53 @@
var: info
- name: Check whether SQLServer User exist
community.general.mssql_script:
mssql_script:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: "SELECT 1 from sys.sql_logins WHERE name='{{ account.username }}';"
when: db_info is succeeded
register: user_exist
- name: Change SQLServer password
community.general.mssql_script:
mssql_script:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; select @@version"
ignore_errors: true
when: user_exist.query_results[0] | length != 0
- name: Add SQLServer user
community.general.mssql_script:
mssql_script:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: "CREATE LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; CREATE USER {{ account.username }} FOR LOGIN {{ account.username }}; select @@version"
ignore_errors: true
when: user_exist.query_results[0] | length == 0
- name: Verify password
community.general.mssql_script:
mssql_script:
login_user: "{{ account.username }}"
login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: |
SELECT @@version
when: check_conn_after_change

View File

@@ -18,6 +18,7 @@
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
group: "{{ params.group if params.group | length > 0 else omit }}"
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: "{{ true if params.groups | length > 0 else false }}"
expires: -1

View File

@@ -28,6 +28,12 @@ params:
default: ''
help_text: "{{ 'Params home help text' | trans }}"
- name: group
type: str
label: "{{ 'Params group label' | trans }}"
default: ''
help_text: "{{ 'Params group help text' | trans }}"
- name: groups
type: str
label: "{{ 'Params groups label' | trans }}"
@@ -61,6 +67,11 @@ i18n:
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
en: 'Default home directory /home/{account username}'
Params group help text:
zh: '请输入用户组(名字或数字),只能输入一个(需填写已存在的用户组)'
ja: 'ユーザー グループ (名前または番号) を入力してください。入力できるのは 1 つだけです (既存のユーザー グループを入力する必要があります)'
en: 'Please enter a user group (name or number), only one can be entered (must fill in an existing user group)'
Params groups help text:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
@@ -86,6 +97,11 @@ i18n:
ja: 'グループ'
en: 'Groups'
Params group label:
zh: '主组'
ja: '主组'
en: 'Main group'
Params uid label:
zh: '用户ID'
ja: 'ユーザーID'

View File

@@ -18,6 +18,7 @@
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
group: "{{ params.group if params.group | length > 0 else omit }}"
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: "{{ true if params.groups | length > 0 else false }}"
expires: -1

View File

@@ -30,6 +30,12 @@ params:
default: ''
help_text: "{{ 'Params home help text' | trans }}"
- name: group
type: str
label: "{{ 'Params group label' | trans }}"
default: ''
help_text: "{{ 'Params group help text' | trans }}"
- name: groups
type: str
label: "{{ 'Params groups label' | trans }}"
@@ -63,6 +69,11 @@ i18n:
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
en: 'Default home directory /home/{account username}'
Params group help text:
zh: '请输入用户组(名字或数字),只能输入一个(需填写已存在的用户组)'
ja: 'ユーザー グループ (名前または番号) を入力してください。入力できるのは 1 つだけです (既存のユーザー グループを入力する必要があります)'
en: 'Please enter a user group (name or number), only one can be entered (must fill in an existing user group)'
Params groups help text:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
@@ -88,6 +99,11 @@ i18n:
ja: 'グループ'
en: 'Groups'
Params group label:
zh: '主组'
ja: '主组'
en: 'Main group'
Params uid label:
zh: '用户ID'
ja: 'ユーザーID'

View File

@@ -9,7 +9,7 @@ from accounts.const import (
AutomationTypes, SecretStrategy, ChangeSecretRecordStatusChoice
)
from accounts.models import ChangeSecretRecord
from accounts.notifications import ChangeSecretExecutionTaskMsg, ChangeSecretReportMsg
from accounts.notifications import ChangeSecretExecutionTaskMsg
from accounts.serializers import ChangeSecretRecordBackUpSerializer
from common.utils import get_logger
from common.utils.file import encrypt_and_compress_zip_file
@@ -94,10 +94,6 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
if not recipients:
return
context = self.get_report_context()
for user in recipients:
ChangeSecretReportMsg(user, context).publish()
if not records:
return

View File

@@ -5,12 +5,14 @@
tasks:
- name: Test SQLServer connection
community.general.mssql_script:
mssql_script:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: |
SELECT
l.name,

View File

@@ -5,12 +5,14 @@
tasks:
- name: Test SQLServer connection
community.general.mssql_script:
mssql_script:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: |
SELECT @@version
register: db_info
@@ -23,47 +25,55 @@
var: info
- name: Check whether SQLServer User exist
community.general.mssql_script:
mssql_script:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: "SELECT 1 from sys.sql_logins WHERE name='{{ account.username }}';"
when: db_info is succeeded
register: user_exist
- name: Change SQLServer password
community.general.mssql_script:
mssql_script:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; select @@version"
ignore_errors: true
when: user_exist.query_results[0] | length != 0
register: change_info
- name: Add SQLServer user
community.general.mssql_script:
mssql_script:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: "CREATE LOGIN [{{ account.username }}] WITH PASSWORD = '{{ account.secret }}'; CREATE USER [{{ account.username }}] FOR LOGIN [{{ account.username }}]; select @@version"
ignore_errors: true
when: user_exist.query_results[0] | length == 0
register: change_info
- name: Verify password
community.general.mssql_script:
mssql_script:
login_user: "{{ account.username }}"
login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: |
SELECT @@version
when: check_conn_after_change

View File

@@ -18,6 +18,7 @@
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
group: "{{ params.group if params.group | length > 0 else omit }}"
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: "{{ true if params.groups | length > 0 else false }}"
expires: -1

View File

@@ -28,6 +28,12 @@ params:
default: ''
help_text: "{{ 'Params home help text' | trans }}"
- name: group
type: str
label: "{{ 'Params group label' | trans }}"
default: ''
help_text: "{{ 'Params group help text' | trans }}"
- name: groups
type: str
label: "{{ 'Params groups label' | trans }}"
@@ -61,6 +67,11 @@ i18n:
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
en: 'Default home directory /home/{account username}'
Params group help text:
zh: '请输入用户组(名字或数字),只能输入一个(需填写已存在的用户组)'
ja: 'ユーザー グループ (名前または番号) を入力してください。入力できるのは 1 つだけです (既存のユーザー グループを入力する必要があります)'
en: 'Please enter a user group (name or number), only one can be entered (must fill in an existing user group)'
Params groups help text:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
@@ -86,6 +97,11 @@ i18n:
ja: 'グループ'
en: 'Groups'
Params group label:
zh: '主组'
ja: '主组'
en: 'Main group'
Params uid label:
zh: '用户ID'
ja: 'ユーザーID'

View File

@@ -18,6 +18,7 @@
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
group: "{{ params.group if params.group | length > 0 else omit }}"
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: "{{ true if params.groups | length > 0 else false }}"
expires: -1

View File

@@ -30,6 +30,12 @@ params:
default: ''
help_text: "{{ 'Params home help text' | trans }}"
- name: group
type: str
label: "{{ 'Params group label' | trans }}"
default: ''
help_text: "{{ 'Params group help text' | trans }}"
- name: groups
type: str
label: "{{ 'Params groups label' | trans }}"
@@ -63,6 +69,11 @@ i18n:
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
en: 'Default home directory /home/{account username}'
Params group help text:
zh: '请输入用户组(名字或数字),只能输入一个(需填写已存在的用户组)'
ja: 'ユーザー グループ (名前または番号) を入力してください。入力できるのは 1 つだけです (既存のユーザー グループを入力する必要があります)'
en: 'Please enter a user group (name or number), only one can be entered (must fill in an existing user group)'
Params groups help text:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
@@ -84,9 +95,14 @@ i18n:
en: 'Home'
Params groups label:
zh: '用户组'
ja: 'グループ'
en: 'Groups'
zh: '附加组'
ja: '追加グループ'
en: 'Additional Group'
Params group label:
zh: '主组'
ja: '主组'
en: 'Main group'
Params uid label:
zh: '用户ID'

View File

@@ -5,11 +5,13 @@
tasks:
- name: "Remove account"
community.general.mssql_script:
mssql_script:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
name: "{{ jms_asset.spec_info.db_name }}"
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: "DROP LOGIN {{ account.username }}; select @@version"

View File

@@ -5,11 +5,13 @@
tasks:
- name: Verify account
community.general.mssql_script:
mssql_script:
login_user: "{{ account.username }}"
login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: |
SELECT @@version

View File

@@ -1,8 +1,5 @@
# -*- 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
@@ -14,6 +11,9 @@ __all__ = ['AZUREVaultClient']
class AZUREVaultClient(object):
def __init__(self, vault_url, tenant_id, client_id, client_secret):
from azure.identity import ClientSecretCredential
from azure.keyvault.secrets import SecretClient
authentication_endpoint = 'https://login.microsoftonline.com/' \
if ('azure.net' in vault_url) else 'https://login.chinacloudapi.cn/'
@@ -23,6 +23,8 @@ class AZUREVaultClient(object):
self.client = SecretClient(vault_url=vault_url, credential=credentials)
def is_active(self):
from azure.core.exceptions import ResourceNotFoundError, ClientAuthenticationError
try:
self.client.set_secret('jumpserver', '666')
except (ResourceNotFoundError, ClientAuthenticationError) as e:
@@ -32,6 +34,8 @@ class AZUREVaultClient(object):
return True, ''
def get(self, name, version=None):
from azure.core.exceptions import ResourceNotFoundError, ClientAuthenticationError
try:
secret = self.client.get_secret(name, version)
return secret.value

View File

@@ -132,7 +132,7 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
return self.asset.platform
@lazyproperty
def alias(self):
def alias(self) -> str:
"""
别称,因为有虚拟账号,@INPUT @MANUAL @USER, 否则为 id
"""
@@ -140,13 +140,13 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
return self.username
return str(self.id)
def is_virtual(self):
def is_virtual(self) -> bool:
"""
不要用 username 去判断,因为可能是构造的 account 对象,设置了同名账号的用户名,
"""
return self.alias.startswith('@')
def is_ds_account(self):
def is_ds_account(self) -> bool:
if self.is_virtual():
return ''
if not self.asset.is_directory_service:
@@ -160,7 +160,7 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
return self.asset.ds
@lazyproperty
def ds_domain(self):
def ds_domain(self) -> str:
"""这个不能去掉perm_account 会动态设置这个值,以更改 full_username"""
if self.is_virtual():
return ''
@@ -172,17 +172,17 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
return '@' in self.username or '\\' in self.username
@property
def full_username(self):
def full_username(self) -> str:
if not self.username_has_domain() and self.ds_domain:
return '{}@{}'.format(self.username, self.ds_domain)
return self.username
@lazyproperty
def has_secret(self):
def has_secret(self) -> bool:
return bool(self.secret)
@lazyproperty
def versions(self):
def versions(self) -> int:
return self.history.count()
def get_su_from_accounts(self):

View File

@@ -33,7 +33,7 @@ class IntegrationApplication(JMSOrgBaseModel):
return qs.filter(*query)
@property
def accounts_amount(self):
def accounts_amount(self) -> int:
return self.get_accounts().count()
@property

View File

@@ -75,11 +75,11 @@ class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
return bool(self.secret)
@property
def has_username(self):
def has_username(self) -> bool:
return bool(self.username)
@property
def spec_info(self):
def spec_info(self) -> dict:
data = {}
if self.secret_type != SecretType.SSH_KEY:
return data
@@ -87,13 +87,13 @@ class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
return data
@property
def password(self):
def password(self) -> str:
if self.secret_type == SecretType.PASSWORD:
return self.secret
return None
@property
def private_key(self):
def private_key(self) -> str:
if self.secret_type == SecretType.SSH_KEY:
return self.secret
return None
@@ -110,7 +110,7 @@ class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
return None
@property
def ssh_key_fingerprint(self):
def ssh_key_fingerprint(self) -> str:
if self.public_key:
public_key = self.public_key
elif self.private_key:

View File

@@ -56,7 +56,7 @@ class VaultModelMixin(models.Model):
__secret = None
@property
def secret(self):
def secret(self) -> str:
if self.__secret:
return self.__secret
from accounts.backends import vault_client

View File

@@ -18,11 +18,11 @@ class VirtualAccount(JMSOrgBaseModel):
verbose_name = _('Virtual account')
@property
def name(self):
def name(self) -> str:
return self.get_alias_display()
@property
def username(self):
def username(self) -> str:
usernames_map = {
AliasAccount.INPUT: _("Manual input"),
AliasAccount.USER: _("Same with user"),
@@ -32,7 +32,7 @@ class VirtualAccount(JMSOrgBaseModel):
return usernames_map.get(self.alias, '')
@property
def comment(self):
def comment(self) -> str:
comments_map = {
AliasAccount.INPUT: _('Non-asset account, Input username/password on connect'),
AliasAccount.USER: _('The account username name same with user on connect'),

View File

@@ -253,6 +253,8 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
'source_id': {'required': False, 'allow_null': True},
}
fields_unimport_template = ['params']
# 手动判断唯一性校验
validators = []
@classmethod
def setup_eager_loading(cls, queryset):
@@ -263,6 +265,21 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
)
return queryset
def validate(self, attrs):
instance = getattr(self, "instance", None)
if instance:
return super().validate(attrs)
field_errors = {}
for _fields in Account._meta.unique_together:
lookup = {field: attrs.get(field) for field in _fields}
if Account.objects.filter(**lookup).exists():
verbose_names = ', '.join([str(Account._meta.get_field(f).verbose_name) for f in _fields])
msg_template = _('Account already exists. Field(s): {fields} must be unique.')
field_errors[_fields[0]] = msg_template.format(fields=verbose_names)
raise serializers.ValidationError(field_errors)
return attrs
class AccountDetailSerializer(AccountSerializer):
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
@@ -456,6 +473,8 @@ class AssetAccountBulkSerializer(
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
spec_info = serializers.DictField(label=_('Spec info'), read_only=True)
class Meta(AccountSerializer.Meta):
fields = AccountSerializer.Meta.fields + ['spec_info']
extra_kwargs = {
@@ -470,6 +489,7 @@ class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
class AccountHistorySerializer(serializers.ModelSerializer):
secret_type = LabeledChoiceField(choices=SecretType.choices, label=_('Secret type'))
secret = serializers.CharField(label=_('Secret'), read_only=True)
id = serializers.IntegerField(label=_('ID'), source='history_id', read_only=True)
class Meta:

View File

@@ -70,6 +70,8 @@ class AuthValidateMixin(serializers.Serializer):
class BaseAccountSerializer(
AuthValidateMixin, ResourceLabelsMixin, BulkOrgResourceModelSerializer
):
spec_info = serializers.DictField(label=_('Spec info'), read_only=True)
class Meta:
model = BaseAccount
fields_mini = ["id", "name", "username"]

View File

@@ -130,7 +130,7 @@ class ChangeSecretRecordSerializer(serializers.ModelSerializer):
read_only_fields = fields
@staticmethod
def get_is_success(obj):
def get_is_success(obj) -> bool:
return obj.status == ChangeSecretRecordStatusChoice.success
@@ -157,7 +157,7 @@ class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer):
read_only_fields = fields
@staticmethod
def get_asset(instance):
def get_asset(instance) -> str:
return str(instance.asset)
@staticmethod
@@ -165,7 +165,7 @@ class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer):
return str(instance.account)
@staticmethod
def get_is_success(obj):
def get_is_success(obj) -> str:
if obj.status == ChangeSecretRecordStatusChoice.success.value:
return _("Success")
return _("Failed")
@@ -196,9 +196,9 @@ class ChangeSecretAccountSerializer(serializers.ModelSerializer):
read_only_fields = fields
@staticmethod
def get_meta(obj):
def get_meta(obj) -> dict:
return account_secret_task_status.get(str(obj.id))
@staticmethod
def get_ttl(obj):
def get_ttl(obj) -> int:
return account_secret_task_status.get_ttl(str(obj.id))

View File

@@ -69,7 +69,7 @@ class AssetRiskSerializer(serializers.Serializer):
risk_summary = serializers.SerializerMethodField()
@staticmethod
def get_risk_summary(obj):
def get_risk_summary(obj) -> dict:
summary = {}
for risk in RiskChoice.choices:
summary[f"{risk[0]}_count"] = obj.get(f"{risk[0]}_count", 0)

View File

@@ -1,36 +0,0 @@
{% load i18n %}
<h3>{% trans 'Task name' %}: {{ name }}</h3>
<h3>{% trans 'Task execution id' %}: {{ execution_id }}</h3>
<p>{% trans 'Respectful' %} {{ recipient }}</p>
<p>{% trans 'Hello! The following is the failure of changing the password of your assets or pushing the account. Please check and handle it in time.' %}</p>
<table style="width: 100%; border-collapse: collapse; max-width: 100%; text-align: left; margin-top: 20px;">
<caption></caption>
<thead>
<tr style="background-color: #f2f2f2;">
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Asset' %}</th>
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Account' %}</th>
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Error' %}</th>
</tr>
</thead>
<tbody>
{% for asset_name, account_username, error in asset_account_errors %}
<tr>
<td style="border: 1px solid #ddd; padding: 10px;">{{ asset_name }}</td>
<td style="border: 1px solid #ddd; padding: 10px;">{{ account_username }}</td>
<td style="border: 1px solid #ddd; padding: 10px;">
<div style="
max-width: 90%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;"
title="{{ error }}"
>
{{ error }}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@@ -3,3 +3,4 @@ from .connect_method import *
from .login_acl import *
from .login_asset_acl import *
from .login_asset_check import *
from .data_masking import *

View File

@@ -0,0 +1,20 @@
from orgs.mixins.api import OrgBulkModelViewSet
from .common import ACLUserFilterMixin
from ..models import DataMaskingRule
from .. import serializers
__all__ = ['DataMaskingRuleViewSet']
class DataMaskingRuleFilter(ACLUserFilterMixin):
class Meta:
model = DataMaskingRule
fields = ('name',)
class DataMaskingRuleViewSet(OrgBulkModelViewSet):
model = DataMaskingRule
filterset_class = DataMaskingRuleFilter
search_fields = ('name',)
serializer_class = serializers.DataMaskingRuleSerializer

View File

@@ -8,7 +8,7 @@ __all__ = ['LoginAssetACLViewSet']
class LoginAssetACLFilter(ACLUserAssetFilterMixin):
class Meta:
model = models.LoginAssetACL
fields = ['name', ]
fields = ['name', 'action']
class LoginAssetACLViewSet(OrgBulkModelViewSet):

View File

@@ -0,0 +1,45 @@
# Generated by Django 4.1.13 on 2025-10-07 16:16
import common.db.fields
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('acls', '0002_auto_20210926_1047'),
]
operations = [
migrations.CreateModel(
name='DataMaskingRule',
fields=[
('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('priority', models.IntegerField(default=50, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')),
('action', models.CharField(default='reject', max_length=64, verbose_name='Action')),
('is_active', models.BooleanField(default=True, verbose_name='Active')),
('users', common.db.fields.JSONManyToManyField(default=dict, to='users.User', verbose_name='Users')),
('assets', common.db.fields.JSONManyToManyField(default=dict, to='assets.Asset', verbose_name='Assets')),
('accounts', models.JSONField(default=list, verbose_name='Accounts')),
('name', models.CharField(max_length=128, verbose_name='Name')),
('fields_pattern', models.CharField(default='password', max_length=128, verbose_name='Fields pattern')),
('masking_method', models.CharField(choices=[('fixed_char', 'Fixed Character Replacement'), ('hide_middle', 'Hide Middle Characters'), ('keep_prefix', 'Keep Prefix Only'), ('keep_suffix', 'Keep Suffix Only')], default='fixed_char', max_length=32, verbose_name='Masking Method')),
('mask_pattern', models.CharField(blank=True, default='######', max_length=128, null=True, verbose_name='Mask Pattern')),
('reviewers', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Reviewers')),
],
options={
'verbose_name': 'Data Masking Rule',
'unique_together': {('org_id', 'name')},
},
),
]

View File

@@ -2,3 +2,4 @@ from .command_acl import *
from .connect_method import *
from .login_acl import *
from .login_asset_acl import *
from .data_masking import *

View File

@@ -0,0 +1,42 @@
from django.db import models
from acls.models import UserAssetAccountBaseACL
from common.utils import get_logger
from django.utils.translation import gettext_lazy as _
logger = get_logger(__file__)
__all__ = ['MaskingMethod', 'DataMaskingRule']
class MaskingMethod(models.TextChoices):
fixed_char = "fixed_char", _("Fixed Character Replacement") # 固定字符替换
hide_middle = "hide_middle", _("Hide Middle Characters") # 隐藏中间几位
keep_prefix = "keep_prefix", _("Keep Prefix Only") # 只保留前缀
keep_suffix = "keep_suffix", _("Keep Suffix Only") # 只保留后缀
class DataMaskingRule(UserAssetAccountBaseACL):
name = models.CharField(max_length=128, verbose_name=_("Name"))
fields_pattern = models.CharField(max_length=128, default='password', verbose_name=_("Fields pattern"))
masking_method = models.CharField(
max_length=32,
choices=MaskingMethod.choices,
default=MaskingMethod.fixed_char,
verbose_name=_("Masking Method"),
)
mask_pattern = models.CharField(
max_length=128,
verbose_name=_("Mask Pattern"),
default="######",
blank=True,
null=True,
)
def __str__(self):
return self.name
class Meta:
unique_together = [('org_id', 'name')]
verbose_name = _("Data Masking Rule")

View File

@@ -1,30 +1,52 @@
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from accounts.models import Account
from acls.models import LoginACL, LoginAssetACL
from assets.models import Asset
from audits.models import UserLoginLog
from common.views.template import custom_render_to_string
from notifications.notifications import UserMessage
from users.models import User
class UserLoginReminderMsg(UserMessage):
subject = _('User login reminder')
template_name = 'acls/user_login_reminder.html'
contexts = [
{"name": "city", "label": _('Login city'), "default": "Shanghai"},
{"name": "username", "label": _('User'), "default": "john"},
{"name": "ip", "label": "IP", "default": "192.168.1.1"},
{"name": "recipient_name", "label": _("Recipient name"), "default": "John"},
{"name": "recipient_username", "label": _("Recipient username"), "default": "john"},
{"name": "user_agent", "label": _('User agent'), "default": "Mozilla/5.0"},
{"name": "acl_name", "label": _('ACL name'), "default": "login acl"},
{"name": "login_from", "label": _('Login from'), "default": "web"},
{"name": "time", "label": _('Login time'), "default": "2025-01-01 12:00:00"},
]
def __init__(self, user, user_log: UserLoginLog):
def __init__(self, user, user_log: UserLoginLog, acl: LoginACL):
self.user_log = user_log
self.acl_name = str(acl)
self.login_from = user_log.get_type_display()
now = timezone.localtime(user_log.datetime)
self.time = now.strftime('%Y-%m-%d %H:%M:%S')
super().__init__(user)
def get_html_msg(self) -> dict:
user_log = self.user_log
context = {
'ip': user_log.ip,
'time': self.time,
'city': user_log.city,
'acl_name': self.acl_name,
'login_from': self.login_from,
'username': user_log.username,
'recipient': self.user,
'recipient_name': self.user.name,
'recipient_username': self.user.username,
'user_agent': user_log.user_agent,
}
message = render_to_string('acls/user_login_reminder.html', context)
message = custom_render_to_string(self.template_name, context)
return {
'subject': str(self.subject),
@@ -40,24 +62,55 @@ class UserLoginReminderMsg(UserMessage):
class AssetLoginReminderMsg(UserMessage):
subject = _('User login alert for asset')
template_name = 'acls/asset_login_reminder.html'
contexts = [
{"name": "city", "label": _('Login city'), "default": "Shanghai"},
{"name": "username", "label": _('User'), "default": "john"},
{"name": "name", "label": _('Name'), "default": "John"},
{"name": "asset", "label": _('Asset'), "default": "dev server"},
{"name": "recipient_name", "label": _('Recipient name'), "default": "John"},
{"name": "recipient_username", "label": _('Recipient username'), "default": "john"},
{"name": "account", "label": _('Account Input username'), "default": "root"},
{"name": "account_name", "label": _('Account name'), "default": "root"},
{"name": "acl_name", "label": _('ACL name'), "default": "login acl"},
{"name": "ip", "label": "IP", "default": "192.168.1.1"},
{"name": "login_from", "label": _('Login from'), "default": "web"},
{"name": "time", "label": _('Login time'), "default": "2025-01-01 12:00:00"}
]
def __init__(self, user, asset: Asset, login_user: User, account: Account, input_username):
def __init__(
self, user, asset: Asset, login_user: User,
account: Account, acl: LoginAssetACL,
ip, input_username, login_from
):
self.ip = ip
self.asset = asset
self.login_user = login_user
self.account = account
self.acl_name = str(acl)
self.login_from = login_from
self.login_user = login_user
self.input_username = input_username
now = timezone.localtime(timezone.now())
self.time = now.strftime('%Y-%m-%d %H:%M:%S')
super().__init__(user)
def get_html_msg(self) -> dict:
context = {
'recipient': self.user,
'ip': self.ip,
'time': self.time,
'login_from': self.login_from,
'recipient_name': self.user.name,
'recipient_username': self.user.username,
'username': self.login_user.username,
'name': self.login_user.name,
'asset': str(self.asset),
'account': self.input_username,
'account_name': self.account.name,
'acl_name': self.acl_name,
}
message = render_to_string('acls/asset_login_reminder.html', context)
message = custom_render_to_string(self.template_name, context)
return {
'subject': str(self.subject),

View File

@@ -3,3 +3,4 @@ from .connect_method import *
from .login_acl import *
from .login_asset_acl import *
from .login_asset_check import *
from .data_masking import *

View File

@@ -90,7 +90,7 @@ class BaseACLSerializer(ActionAclSerializer, serializers.Serializer):
fields_small = fields_mini + [
"is_active", "priority", "action",
"date_created", "date_updated",
"comment", "created_by", "org_id",
"comment", "created_by"
]
fields_m2m = ["reviewers", ]
fields = fields_small + fields_m2m
@@ -100,6 +100,20 @@ class BaseACLSerializer(ActionAclSerializer, serializers.Serializer):
'reviewers': {'label': _('Recipients')},
}
class BaseUserACLSerializer(BaseACLSerializer):
users = JSONManyToManyField(label=_('User'))
class Meta(BaseACLSerializer.Meta):
fields = BaseACLSerializer.Meta.fields + ['users']
class BaseUserAssetAccountACLSerializer(BaseUserACLSerializer):
assets = JSONManyToManyField(label=_('Asset'))
accounts = serializers.ListField(label=_('Account'))
class Meta(BaseUserACLSerializer.Meta):
fields = BaseUserACLSerializer.Meta.fields + ['assets', 'accounts', 'org_id']
def validate_reviewers(self, reviewers):
action = self.initial_data.get('action')
if not action and self.instance:
@@ -118,19 +132,4 @@ class BaseACLSerializer(ActionAclSerializer, serializers.Serializer):
"None of the reviewers belong to Organization `{}`".format(org.name)
)
raise serializers.ValidationError(error)
return valid_reviewers
class BaseUserACLSerializer(BaseACLSerializer):
users = JSONManyToManyField(label=_('User'))
class Meta(BaseACLSerializer.Meta):
fields = BaseACLSerializer.Meta.fields + ['users']
class BaseUserAssetAccountACLSerializer(BaseUserACLSerializer):
assets = JSONManyToManyField(label=_('Asset'))
accounts = serializers.ListField(label=_('Account'))
class Meta(BaseUserACLSerializer.Meta):
fields = BaseUserACLSerializer.Meta.fields + ['assets', 'accounts']
return valid_reviewers

View File

@@ -1,4 +1,4 @@
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.serializers.mixin import CommonBulkModelSerializer
from .base import BaseUserAssetAccountACLSerializer as BaseSerializer
from ..const import ActionChoices
from ..models import ConnectMethodACL
@@ -6,16 +6,15 @@ from ..models import ConnectMethodACL
__all__ = ["ConnectMethodACLSerializer"]
class ConnectMethodACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer):
class ConnectMethodACLSerializer(BaseSerializer, CommonBulkModelSerializer):
class Meta(BaseSerializer.Meta):
model = ConnectMethodACL
fields = [
i for i in BaseSerializer.Meta.fields + ['connect_methods']
if i not in ['assets', 'accounts']
if i not in ['assets', 'accounts', 'org_id']
]
action_choices_exclude = BaseSerializer.Meta.action_choices_exclude + [
ActionChoices.review,
ActionChoices.accept,
ActionChoices.notice,
ActionChoices.face_verify,
ActionChoices.face_online,

View File

@@ -0,0 +1,19 @@
from django.utils.translation import gettext_lazy as _
from acls.models import MaskingMethod, DataMaskingRule
from common.serializers.fields import LabeledChoiceField
from common.serializers.mixin import CommonBulkModelSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .base import BaseUserAssetAccountACLSerializer as BaseSerializer
__all__ = ['DataMaskingRuleSerializer']
class DataMaskingRuleSerializer(BaseSerializer, BulkOrgResourceModelSerializer):
masking_method = LabeledChoiceField(
choices=MaskingMethod.choices, default=MaskingMethod.fixed_char, label=_('Masking Method')
)
class Meta(BaseSerializer.Meta):
model = DataMaskingRule
fields = BaseSerializer.Meta.fields + ['fields_pattern', 'masking_method', 'mask_pattern']

View File

@@ -1,7 +1,7 @@
from django.utils.translation import gettext as _
from common.serializers import CommonBulkModelSerializer
from common.serializers import MethodSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .base import BaseUserACLSerializer
from .rules import RuleSerializer
from ..const import ActionChoices
@@ -12,12 +12,12 @@ __all__ = ["LoginACLSerializer"]
common_help_text = _("With * indicating a match all. ")
class LoginACLSerializer(BaseUserACLSerializer, BulkOrgResourceModelSerializer):
class LoginACLSerializer(BaseUserACLSerializer, CommonBulkModelSerializer):
rules = MethodSerializer(label=_('Rule'))
class Meta(BaseUserACLSerializer.Meta):
model = LoginACL
fields = BaseUserACLSerializer.Meta.fields + ['rules', ]
fields = list((set(BaseUserACLSerializer.Meta.fields) | {'rules'}))
action_choices_exclude = [
ActionChoices.warning,
ActionChoices.notify_and_warn,

View File

@@ -1,13 +1,17 @@
{% load i18n %}
<h3>{% trans 'Dear' %}: {{ recipient.name }}[{{ recipient.username }}]</h3>
<h3>{% trans 'Dear' %}: {{ recipient_name }}[{{ recipient_username }}]</h3>
<hr>
<p>{% trans 'We would like to inform you that a user has recently logged into the following asset:' %}<p>
<p><strong>{% trans 'Asset details' %}:</strong></p>
<ul>
<li><strong>{% trans 'User' %}:</strong> [{{ name }}({{ username }})]</li>
<li><strong>IP:</strong> [{{ ip }}]</li>
<li><strong>{% trans 'Assets' %}:</strong> [{{ asset }}]</li>
<li><strong>{% trans 'Account' %}:</strong> [{{ account_name }}({{ account }})]</li>
<li><strong>{% trans 'Login asset acl' %}:</strong> [{{ acl_name }}]</li>
<li><strong>{% trans 'Login from' %}:</strong> [{{ login_from }}]</li>
<li><strong>{% trans 'Time' %}:</strong> [{{ time }}]</li>
</ul>
<hr>

View File

@@ -1,6 +1,6 @@
{% load i18n %}
<h3>{% trans 'Dear' %}: {{ recipient.name }}[{{ recipient.username }}]</h3>
<h3>{% trans 'Dear' %}: {{ recipient_name }}[{{ recipient_username }}]</h3>
<hr>
<p>{% trans 'We would like to inform you that a user has recently logged:' %}<p>
<p><strong>{% trans 'User details' %}:</strong></p>
@@ -8,7 +8,10 @@
<li><strong>{% trans 'User' %}:</strong> [{{ username }}]</li>
<li><strong>IP:</strong> [{{ ip }}]</li>
<li><strong>{% trans 'Login city' %}:</strong> [{{ city }}]</li>
<li><strong>{% trans 'Login from' %}:</strong> [{{ login_from }}]</li>
<li><strong>{% trans 'User agent' %}:</strong> [{{ user_agent }}]</li>
<li><strong>{% trans 'Login acl' %}:</strong> [{{ acl_name }}]</li>
<li><strong>{% trans 'Time' %}:</strong> [{{ time }}]</li>
</ul>
<hr>

View File

@@ -11,6 +11,7 @@ router.register(r'login-asset-acls', api.LoginAssetACLViewSet, 'login-asset-acl'
router.register(r'command-filter-acls', api.CommandFilterACLViewSet, 'command-filter-acl')
router.register(r'command-groups', api.CommandGroupViewSet, 'command-group')
router.register(r'connect-method-acls', api.ConnectMethodACLViewSet, 'connect-method-acl')
router.register(r'data-masking-rules', api.DataMaskingRuleViewSet, 'data-masking-rule')
urlpatterns = [
path('login-asset/check/', api.LoginAssetCheckAPI.as_view(), name='login-asset-check'),

View File

@@ -14,6 +14,7 @@ class FavoriteAssetViewSet(BulkModelViewSet):
serializer_class = FavoriteAssetSerializer
permission_classes = (IsValidUser,)
filterset_fields = ['asset']
page_no_limit = True
def dispatch(self, request, *args, **kwargs):
with tmp_to_root_org():

View File

@@ -7,7 +7,7 @@ from rest_framework.decorators import action
from rest_framework.response import Response
from assets.const import AllTypes
from assets.models import Platform, Node, Asset, PlatformProtocol
from assets.models import Platform, Node, Asset, PlatformProtocol, PlatformAutomation
from assets.serializers import PlatformSerializer, PlatformProtocolSerializer, PlatformListSerializer
from common.api import JMSModelViewSet
from common.permissions import IsValidUser
@@ -43,6 +43,7 @@ class AssetPlatformViewSet(JMSModelViewSet):
'ops_methods': 'assets.view_platform',
'filter_nodes_assets': 'assets.view_platform',
}
page_no_limit = True
def get_queryset(self):
# 因为没有走分页逻辑,所以需要这里 prefetch
@@ -112,6 +113,7 @@ class PlatformProtocolViewSet(JMSModelViewSet):
class PlatformAutomationMethodsApi(generics.ListAPIView):
permission_classes = (IsValidUser,)
queryset = PlatformAutomation.objects.none()
@staticmethod
def automation_methods():

View File

@@ -161,6 +161,7 @@ class CategoryTreeApi(SerializeToTreeNodeMixin, generics.ListAPIView):
'GET': 'assets.view_asset',
'list': 'assets.view_asset',
}
queryset = Node.objects.none()
def get_assets(self):
key = self.request.query_params.get('key')

View File

@@ -6,11 +6,13 @@
tasks:
- name: Test SQLServer connection
community.general.mssql_script:
mssql_script:
login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: |
SELECT @@version

View File

@@ -1,5 +1,6 @@
from django.utils.translation import gettext_lazy as _
from orgs.models import Organization
from .base import BaseType
@@ -52,3 +53,41 @@ class GPTTypes(BaseType):
return [
cls.CHATGPT,
]
CHATX_NAME = 'ChatX'
def create_or_update_chatx_resources(chatx_name=CHATX_NAME, org_id=Organization.SYSTEM_ID):
from django.apps import apps
platform_model = apps.get_model('assets', 'Platform')
asset_model = apps.get_model('assets', 'Asset')
account_model = apps.get_model('accounts', 'Account')
platform, __ = platform_model.objects.get_or_create(
name=chatx_name,
defaults={
'internal': True,
'type': chatx_name,
'category': 'ai',
}
)
asset, __ = asset_model.objects.get_or_create(
address=chatx_name,
defaults={
'name': chatx_name,
'platform': platform,
'org_id': org_id
}
)
account, __ = account_model.objects.get_or_create(
username=chatx_name,
defaults={
'name': chatx_name,
'asset': asset,
'org_id': org_id
}
)
return asset, account

View File

@@ -250,6 +250,12 @@ class Protocol(ChoicesMixin, models.TextChoices):
'default': False,
'label': _('Auth username')
},
'enable_cluster_mode': {
'type': 'bool',
'default': False,
'label': _('Enable cluster mode'),
'help_text': _('Enable if this Redis instance is part of a cluster')
},
}
},
}

View File

@@ -112,7 +112,7 @@ class Protocol(models.Model):
return protocols[0] if len(protocols) > 0 else {}
@property
def setting(self):
def setting(self) -> dict:
if self._setting is not None:
return self._setting
return self.asset_platform_protocol.get('setting', {})
@@ -122,7 +122,7 @@ class Protocol(models.Model):
self._setting = value
@property
def public(self):
def public(self) -> bool:
return self.asset_platform_protocol.get('public', True)
@@ -210,7 +210,7 @@ class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin,
return self.category == const.Category.DS and hasattr(self, 'ds')
@lazyproperty
def spec_info(self):
def spec_info(self) -> dict:
instance = getattr(self, self.category, None)
if not instance:
return {}
@@ -240,7 +240,7 @@ class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin,
return info
@lazyproperty
def auto_config(self):
def auto_config(self) -> dict:
platform = self.platform
auto_config = {
'su_enabled': platform.su_enabled,
@@ -343,11 +343,11 @@ class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin,
return names
@lazyproperty
def type(self):
def type(self) -> str:
return self.platform.type
@lazyproperty
def category(self):
def category(self) -> str:
return self.platform.category
def is_category(self, category):

View File

@@ -573,7 +573,7 @@ class Node(JMSOrgBaseModel, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
return not self.__gt__(other)
@property
def name(self):
def name(self) -> str:
return self.value
def computed_full_value(self):

View File

@@ -25,7 +25,7 @@ class PlatformProtocol(models.Model):
return '{}/{}'.format(self.name, self.port)
@property
def secret_types(self):
def secret_types(self) -> list:
return Protocol.settings().get(self.name, {}).get('secret_types', ['password'])
@lazyproperty

View File

@@ -147,6 +147,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, Writa
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=())
accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, write_only=True, label=_('Accounts'))
nodes_display = NodeDisplaySerializer(read_only=False, required=False, label=_("Node path"))
auto_config = serializers.DictField(read_only=True, label=_('Auto info'))
platform = ObjectRelatedField(queryset=Platform.objects, required=True, label=_('Platform'),
attrs=('id', 'name', 'type'))
accounts_amount = serializers.IntegerField(read_only=True, label=_('Accounts amount'))
@@ -425,6 +426,18 @@ class DetailMixin(serializers.Serializer):
gathered_info = MethodSerializer(label=_('Gathered info'), read_only=True)
auto_config = serializers.DictField(read_only=True, label=_('Auto info'))
@staticmethod
def get_auto_config(obj) -> dict:
return obj.auto_config
@staticmethod
def get_gathered_info(obj) -> dict:
return obj.gathered_info
@staticmethod
def get_spec_info(obj) -> dict:
return obj.spec_info
def get_instance(self):
request = self.context.get('request')
if not self.instance and UUID_PATTERN.findall(request.path):

View File

@@ -59,7 +59,10 @@ class DatabaseSerializer(AssetSerializer):
if not platform:
return
if platform.type in ['mysql', 'mariadb']:
if platform.type in [
'mysql', 'mariadb', 'oracle', 'sqlserver',
'db2', 'dameng', 'clickhouse', 'redis'
]:
db_field.required = False
db_field.allow_blank = True
db_field.allow_null = True

View File

@@ -26,4 +26,13 @@ class WebSerializer(AssetSerializer):
'submit_selector': {
'default': 'id=login_button',
},
'script': {
'default': [],
}
}
def to_internal_value(self, data):
data = data.copy()
if data.get('script') in ("", None):
data.pop('script', None)
return super().to_internal_value(data)

View File

@@ -19,11 +19,13 @@ __all__ = [
class BaseAutomationSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer):
assets = ObjectRelatedField(many=True, required=False, queryset=Asset.objects, label=_('Assets'))
nodes = ObjectRelatedField(many=True, required=False, queryset=Node.objects, label=_('Nodes'))
executed_amount = serializers.IntegerField(read_only=True, label=_('Executed amount'))
class Meta:
read_only_fields = [
'date_created', 'date_updated', 'created_by',
'periodic_display', 'executed_amount', 'type', 'last_execution_date'
'periodic_display', 'executed_amount', 'type',
'last_execution_date',
]
mini_fields = [
'id', 'name', 'type', 'is_periodic', 'interval',

View File

@@ -1,6 +1,6 @@
import os
import uuid
from datetime import timedelta
from datetime import timedelta, datetime
from importlib import import_module
from django.conf import settings
@@ -40,7 +40,7 @@ __all__ = [
class JobLog(JobExecution):
@property
def creator_name(self):
def creator_name(self) -> str:
return self.creator.name
class Meta:
@@ -232,7 +232,7 @@ class UserLoginLog(models.Model):
return '%s(%s)' % (self.username, self.city)
@property
def backend_display(self):
def backend_display(self) -> str:
return gettext(self.backend)
@classmethod
@@ -258,7 +258,7 @@ class UserLoginLog(models.Model):
return login_logs
@property
def reason_display(self):
def reason_display(self) -> str:
from authentication.errors import reason_choices, old_reason_choices
reason = reason_choices.get(self.reason)
@@ -300,15 +300,15 @@ class UserSession(models.Model):
return '%s(%s)' % (self.user, self.ip)
@property
def backend_display(self):
def backend_display(self) -> str:
return gettext(self.backend)
@property
def is_active(self):
def is_active(self) -> bool:
return user_session_manager.check_active(self.key)
@property
def date_expired(self):
def date_expired(self) -> datetime:
session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore
session_store = session_store_cls(session_key=self.key)
cache_key = session_store.cache_key

View File

@@ -119,11 +119,11 @@ class OperateLogSerializer(BulkOrgResourceModelSerializer):
fields = fields_small
@staticmethod
def get_resource_type(instance):
def get_resource_type(instance) -> str:
return _(instance.resource_type)
@staticmethod
def get_resource(instance):
def get_resource(instance) -> str:
return i18n_trans(instance.resource)
@@ -147,11 +147,11 @@ class ActivityUnionLogSerializer(serializers.Serializer):
r_type = serializers.CharField(read_only=True)
@staticmethod
def get_timestamp(obj):
def get_timestamp(obj) -> str:
return as_current_tz(obj['datetime']).strftime('%Y-%m-%d %H:%M:%S')
@staticmethod
def get_content(obj):
def get_content(obj) -> str:
if not obj['r_detail']:
action = obj['r_action'].replace('_', ' ').capitalize()
ctn = _('%s %s this resource') % (obj['r_user'], _(action).lower())
@@ -160,7 +160,7 @@ class ActivityUnionLogSerializer(serializers.Serializer):
return ctn
@staticmethod
def get_detail_url(obj):
def get_detail_url(obj) -> str:
detail_url = ''
detail_id, obj_type = obj['r_detail_id'], obj['r_type']
if not detail_id:
@@ -210,7 +210,7 @@ class UserSessionSerializer(serializers.ModelSerializer):
"backend_display": {"label": _("Auth backend display")},
}
def get_is_current_user_session(self, obj):
def get_is_current_user_session(self, obj) -> bool:
request = self.context.get('request')
if not request:
return False

View File

@@ -116,7 +116,7 @@ def send_login_info_to_reviewers(instance: UserLoginLog | str, auth_acl_id):
reviewers = acl.reviewers.all()
for reviewer in reviewers:
UserLoginReminderMsg(reviewer, instance).publish_async()
UserLoginReminderMsg(reviewer, instance, acl).publish_async()
@receiver(post_auth_success)

View File

@@ -2,15 +2,14 @@
#
import datetime
import os
import subprocess
from celery import shared_task
from django.conf import settings
from django.core.files.storage import default_storage
from django.db import transaction
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.utils._os import safe_join
from django.utils.translation import gettext_lazy as _
from common.const.crontab import CRONTAB_AT_AM_TWO
from common.storage.ftp_file import FTPFileStorageHandler
@@ -79,7 +78,7 @@ def clean_celery_tasks_period():
command = "find %s -mtime +%s -name '*.log' -type f -exec rm -f {} \\;"
safe_run_cmd(command, (settings.CELERY_LOG_DIR, expire_days))
celery_log_path = safe_join(settings.LOG_DIR, 'celery.log')
command = "echo > {}".format(celery_log_path)
command = "echo > %s"
safe_run_cmd(command, (celery_log_path,))

View File

@@ -69,6 +69,8 @@ class RDPFileClientProtocolURLMixin:
'autoreconnection enabled:i': '1',
'bookmarktype:i': '3',
'use redirection server name:i': '0',
'bitmapcachepersistenable:i': '0',
'bitmapcachesize:i': '1500',
}
# copy from
@@ -76,7 +78,6 @@ class RDPFileClientProtocolURLMixin:
rdp_low_speed_broadband_option = {
"connection type:i": 2,
"disable wallpaper:i": 1,
"bitmapcachepersistenable:i": 1,
"disable full window drag:i": 1,
"disable menu anims:i": 1,
"allow font smoothing:i": 0,
@@ -87,7 +88,6 @@ class RDPFileClientProtocolURLMixin:
rdp_high_speed_broadband_option = {
"connection type:i": 4,
"disable wallpaper:i": 0,
"bitmapcachepersistenable:i": 1,
"disable full window drag:i": 1,
"disable menu anims:i": 0,
"allow font smoothing:i": 0,
@@ -362,6 +362,7 @@ class ConnectionTokenViewSet(AuthFaceMixin, ExtraActionApiMixin, RootOrgViewMixi
self.validate_serializer(serializer)
return super().perform_create(serializer)
def _insert_connect_options(self, data, user):
connect_options = data.pop('connect_options', {})
default_name_opts = {
@@ -375,7 +376,7 @@ class ConnectionTokenViewSet(AuthFaceMixin, ExtraActionApiMixin, RootOrgViewMixi
for name in default_name_opts.keys():
value = preferences.get(name, default_name_opts[name])
connect_options[name] = value
connect_options['lang'] = getattr(user, 'lang', settings.LANGUAGE_CODE)
connect_options['lang'] = getattr(user, 'lang') or settings.LANGUAGE_CODE
data['connect_options'] = connect_options
@staticmethod
@@ -431,7 +432,7 @@ class ConnectionTokenViewSet(AuthFaceMixin, ExtraActionApiMixin, RootOrgViewMixi
if account.username != AliasAccount.INPUT:
data['input_username'] = ''
ticket = self._validate_acl(user, asset, account, connect_method)
ticket = self._validate_acl(user, asset, account, connect_method, protocol)
if ticket:
data['from_ticket'] = ticket
@@ -470,7 +471,7 @@ class ConnectionTokenViewSet(AuthFaceMixin, ExtraActionApiMixin, RootOrgViewMixi
after=after, object_name=object_name
)
def _validate_acl(self, user, asset, account, connect_method):
def _validate_acl(self, user, asset, account, connect_method, protocol):
from acls.models import LoginAssetACL
kwargs = {'user': user, 'asset': asset, 'account': account}
if account.username == AliasAccount.INPUT:
@@ -523,9 +524,15 @@ class ConnectionTokenViewSet(AuthFaceMixin, ExtraActionApiMixin, RootOrgViewMixi
return
self._record_operate_log(acl, asset)
os = get_request_os(self.request) if self.request else 'windows'
method = ConnectMethodUtil.get_connect_method(
connect_method, protocol=protocol, os=os
)
login_from = method['label'] if method else connect_method
for reviewer in reviewers:
AssetLoginReminderMsg(
reviewer, asset, user, account, self.input_username
reviewer, asset, user, account, acl,
ip, self.input_username, login_from
).publish_async()
def create_face_verify(self, response):
@@ -558,7 +565,9 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
rbac_perms = {
'create': 'authentication.add_superconnectiontoken',
'renewal': 'authentication.add_superconnectiontoken',
'list': 'authentication.view_superconnectiontoken',
'check': 'authentication.view_superconnectiontoken',
'retrieve': 'authentication.view_superconnectiontoken',
'get_secret_detail': 'authentication.view_superconnectiontokensecret',
'get_applet_info': 'authentication.view_superconnectiontoken',
'release_applet_account': 'authentication.view_superconnectiontoken',
@@ -566,7 +575,12 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
}
def get_queryset(self):
return ConnectionToken.objects.all()
return ConnectionToken.objects.none()
def get_object(self):
pk = self.kwargs.get(self.lookup_field)
token = get_object_or_404(ConnectionToken, pk=pk)
return token
def get_user(self, serializer):
return serializer.validated_data.get('user')
@@ -618,6 +632,8 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
token_id = request.data.get('id') or ''
token = ConnectionToken.get_typed_connection_token(token_id)
if not token:
raise PermissionDenied('Token {} is not valid'.format(token))
token.is_valid()
serializer = self.get_serializer(instance=token)

View File

@@ -67,8 +67,9 @@ class UserResetPasswordSendCodeApi(CreateAPIView):
code = random_string(settings.SMS_CODE_LENGTH, lower=False, upper=False)
subject = '%s: %s' % (get_login_title(), _('Forgot password'))
tip = _('The validity period of the verification code is {} minute').format(settings.VERIFY_CODE_TTL // 60)
context = {
'user': user, 'title': subject, 'code': code,
'user': user, 'title': subject, 'code': code, 'tip': tip,
}
message = render_to_string('authentication/_msg_reset_password_code.html', context)
content = {'subject': subject, 'message': message}

View File

@@ -25,7 +25,10 @@ class JMSBaseAuthBackend:
"""
# 三方用户认证完成后,在后续的 get_user 获取逻辑中,也应该需要检查用户是否有效
is_valid = getattr(user, 'is_valid', None)
return is_valid or is_valid is None
if not is_valid:
logger.info("User %s is not valid", getattr(user, "username", "<unknown>"))
return False
return True
# allow user to authenticate
def username_allow_authenticate(self, username):

View File

@@ -136,7 +136,7 @@ class SignatureAuthentication(signature.SignatureAuthentication):
# example implementation:
try:
key = AccessKey.objects.get(id=key_id)
if not key.is_active:
if not key.is_valid:
return None, None
user, secret = key.user, str(key.secret)
after_authenticate_update_date(user, key)

View File

@@ -134,6 +134,7 @@ class OIDCAuthCallbackView(View, FlashMessageMixin):
log_prompt = "Process GET requests [OIDCAuthCallbackView]: {}"
logger.debug(log_prompt.format('Start'))
callback_params = request.GET
error_title = _("OpenID Error")
# Retrieve the state value that was previously generated. No state means that we cannot
# authenticate the user (so a failure should be returned).
@@ -172,10 +173,9 @@ class OIDCAuthCallbackView(View, FlashMessageMixin):
try:
user = auth.authenticate(nonce=nonce, request=request, code_verifier=code_verifier)
except IntegrityError as e:
title = _("OpenID Error")
msg = _('Please check if a user with the same username or email already exists')
logger.error(e, exc_info=True)
response = self.get_failed_response('/', title, msg)
response = self.get_failed_response('/', error_title, msg)
return response
if user:
logger.debug(log_prompt.format('Login: {}'.format(user)))
@@ -194,7 +194,6 @@ class OIDCAuthCallbackView(View, FlashMessageMixin):
return HttpResponseRedirect(
next_url or settings.AUTH_OPENID_AUTHENTICATION_REDIRECT_URI
)
if 'error' in callback_params:
logger.debug(
log_prompt.format('Error in callback params: {}'.format(callback_params['error']))
@@ -205,9 +204,12 @@ class OIDCAuthCallbackView(View, FlashMessageMixin):
# OpenID Connect Provider authenticate endpoint.
logger.debug(log_prompt.format('Logout'))
auth.logout(request)
redirect_url = settings.AUTH_OPENID_AUTHENTICATION_FAILURE_REDIRECT_URI
if not user and getattr(request, 'error_message', ''):
response = self.get_failed_response(redirect_url, title=error_title, msg=request.error_message)
return response
logger.debug(log_prompt.format('Redirect'))
return HttpResponseRedirect(settings.AUTH_OPENID_AUTHENTICATION_FAILURE_REDIRECT_URI)
return HttpResponseRedirect(redirect_url)
class OIDCAuthCallbackClientView(BaseAuthCallbackClientView):

View File

@@ -14,7 +14,9 @@ class TempTokenAuthBackend(JMSBaseAuthBackend):
return settings.AUTH_TEMP_TOKEN
def authenticate(self, request, username='', password=''):
token = self.model.objects.filter(username=username, secret=password).first()
tokens = self.model.objects.filter(username=username).order_by('-date_created')[:500]
token = next((t for t in tokens if t.secret == password), None)
if not token:
return None
if not token.is_valid:

View File

@@ -38,7 +38,7 @@ class BaseMFA(abc.ABC):
if not ok:
return False, msg
cache.set(cache_key, code, 60)
cache.set(cache_key, code, settings.VERIFY_CODE_TTL)
return True, msg
def is_authenticated(self):

View File

@@ -39,13 +39,14 @@ class MFAEmail(BaseMFA):
def send_challenge(self):
code = random_string(settings.SMS_CODE_LENGTH, lower=False, upper=False)
subject = '%s: %s' % (get_login_title(), _('MFA code'))
tip = _('The validity period of the verification code is {} minute').format(settings.VERIFY_CODE_TTL // 60)
context = {
'user': self.user, 'title': subject, 'code': code,
'user': self.user, 'title': subject, 'code': code, 'tip': tip,
}
message = render_to_string('authentication/_msg_mfa_email_code.html', context)
content = {'subject': subject, 'message': message}
sender_util = SendAndVerifyCodeUtil(
self.user.email, code=code, backend=self.name, timeout=60, **content
self.user.email, code=code, backend=self.name, **content
)
sender_util.gen_and_send_async()

View File

@@ -4,6 +4,24 @@ import authentication.models.access_key
import common.db.fields
from django.db import migrations
old_access_key_secrets_mapper = {}
def fetch_access_key_secrets(apps, schema_editor):
AccessKey = apps.get_model("authentication", "AccessKey")
for id, secret in AccessKey.objects.all().values_list('id', 'secret'):
old_access_key_secrets_mapper[str(id)] = secret
def save_access_key_secrets(apps, schema_editor):
AccessKey = apps.get_model("authentication", "AccessKey")
aks = AccessKey.objects.filter(id__in=list(old_access_key_secrets_mapper.keys()))
for ak in aks:
old_value = old_access_key_secrets_mapper.get(str(ak.id))
if not old_value:
continue
ak.secret = old_value
ak.save(update_fields=["secret"])
class Migration(migrations.Migration):
@@ -12,6 +30,7 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RunPython(fetch_access_key_secrets),
migrations.AlterField(
model_name="accesskey",
name="secret",
@@ -27,4 +46,5 @@ class Migration(migrations.Migration):
verbose_name="Secret"
),
),
migrations.RunPython(save_access_key_secrets),
]

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
#
import inspect
import threading
import time
import uuid
from functools import partial
@@ -12,6 +13,7 @@ from django.contrib.auth import (
BACKEND_SESSION_KEY, load_backend,
PermissionDenied, user_login_failed, _clean_credentials,
)
from django.contrib.auth import get_user_model
from django.core.cache import cache
from django.core.exceptions import ImproperlyConfigured
from django.shortcuts import reverse, redirect, get_object_or_404
@@ -46,6 +48,10 @@ def _get_backends(return_tuples=False):
return backends
class OnlyAllowExistUserAuthError(Exception):
pass
auth._get_backends = _get_backends
@@ -54,6 +60,24 @@ def authenticate(request=None, **credentials):
If the given credentials are valid, return a User object.
之所以 hack 这个 authenticate
"""
UserModel = get_user_model()
original_get_or_create = UserModel.objects.get_or_create
thread_local = threading.local()
thread_local.thread_id = threading.get_ident()
def custom_get_or_create(self, *args, **kwargs):
logger.debug(f"get_or_create: thread_id={threading.get_ident()}, username={username}")
if threading.get_ident() != thread_local.thread_id or not settings.ONLY_ALLOW_EXIST_USER_AUTH:
return original_get_or_create(*args, **kwargs)
create_username = kwargs.get('username')
try:
UserModel.objects.get(username=create_username)
except UserModel.DoesNotExist:
raise OnlyAllowExistUserAuthError
return original_get_or_create(*args, **kwargs)
username = credentials.get('username')
temp_user = None
@@ -71,10 +95,19 @@ def authenticate(request=None, **credentials):
# This backend doesn't accept these credentials as arguments. Try the next one.
continue
try:
UserModel.objects.get_or_create = custom_get_or_create.__get__(UserModel.objects)
user = backend.authenticate(request, **credentials)
except PermissionDenied:
# This backend says to stop in our tracks - this user should not be allowed in at all.
break
except OnlyAllowExistUserAuthError:
request.error_message = _(
'''The administrator has enabled "Only allow existing users to log in",
and the current user is not in the user list. Please contact the administrator.'''
)
continue
finally:
UserModel.objects.get_or_create = original_get_or_create
if user is None:
continue

View File

@@ -25,6 +25,10 @@ class AccessKey(models.Model):
date_last_used = models.DateTimeField(null=True, blank=True, verbose_name=_('Date last used'))
date_created = models.DateTimeField(auto_now_add=True)
@property
def is_valid(self):
return self.is_active and self.user.is_valid
def get_id(self):
return str(self.id)

View File

@@ -4,6 +4,7 @@ from datetime import timedelta
from django.conf import settings
from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.db import models
from django.shortcuts import get_object_or_404
from django.utils import timezone
@@ -76,7 +77,10 @@ class ConnectionToken(JMSOrgBaseModel):
@classmethod
def get_typed_connection_token(cls, token_id):
token = get_object_or_404(cls, id=token_id)
try:
token = get_object_or_404(cls, id=token_id)
except ValidationError:
return None
if token.type == ConnectionTokenType.ADMIN.value:
token = AdminConnectionToken.objects.get(id=token_id)
@@ -85,11 +89,11 @@ class ConnectionToken(JMSOrgBaseModel):
return token
@property
def is_expired(self):
def is_expired(self) -> bool:
return self.date_expired < timezone.now()
@property
def expire_time(self):
def expire_time(self) -> int:
interval = self.date_expired - timezone.now()
seconds = interval.total_seconds()
if seconds < 0:
@@ -161,7 +165,7 @@ class ConnectionToken(JMSOrgBaseModel):
def expire_at(self):
return self.permed_account.date_expired.timestamp()
def is_valid(self):
def is_valid(self) -> bool:
if not self.is_active:
error = _('Connection token inactive')
raise PermissionDenied(error)
@@ -334,6 +338,18 @@ class ConnectionToken(JMSOrgBaseModel):
acls = CommandFilterACL.filter_queryset(**kwargs).valid()
return acls
@lazyproperty
def data_masking_rules(self):
from acls.models import DataMaskingRule
kwargs = {
'user': self.user,
'asset': self.asset,
'account': self.account_object,
}
with tmp_to_org(self.asset.org_id):
rules = DataMaskingRule.filter_queryset(**kwargs).valid()
return rules
class SuperConnectionToken(ConnectionToken):
_type = ConnectionTokenType.SUPER

View File

@@ -1,14 +1,24 @@
from django.template.loader import render_to_string
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from common.utils import get_logger
from common.utils.timezone import local_now_display
from common.views.template import custom_render_to_string
from notifications.notifications import UserMessage
logger = get_logger(__file__)
class DifferentCityLoginMessage(UserMessage):
subject = _('Different city login reminder')
template_name = 'authentication/_msg_different_city.html'
contexts = [
{"name": "city", "label": _('Login city'), "default": "Shanghai"},
{"name": "username", "label": _('User'), "default": "john"},
{"name": "name", "label": _('Name'), "default": "John"},
{"name": "ip", "label": "IP", "default": "192.168.1.1"},
{"name": "time", "label": _('Login Date'), "default": "2025-01-01 12:00:00"},
]
def __init__(self, user, ip, city):
self.ip = ip
self.city = city
@@ -16,18 +26,16 @@ class DifferentCityLoginMessage(UserMessage):
def get_html_msg(self) -> dict:
now = local_now_display()
subject = _('Different city login reminder')
context = dict(
subject=subject,
name=self.user.name,
username=self.user.username,
ip=self.ip,
time=now,
city=self.city,
)
message = render_to_string('authentication/_msg_different_city.html', context)
message = custom_render_to_string(self.template_name, context)
return {
'subject': subject,
'subject': str(self.subject),
'message': message
}
@@ -41,6 +49,16 @@ class DifferentCityLoginMessage(UserMessage):
class OAuthBindMessage(UserMessage):
subject = _('OAuth binding reminder')
template_name = 'authentication/_msg_oauth_bind.html'
contexts = [
{"name": "username", "label": _('User'), "default": "john"},
{"name": "name", "label": _('Name'), "default": "John"},
{"name": "ip", "label": "IP", "default": "192.168.1.1"},
{"name": "oauth_name", "label": _('OAuth name'), "default": "WeCom"},
{"name": "oauth_id", "label": _('OAuth ID'), "default": "000001"},
]
def __init__(self, user, ip, oauth_name, oauth_id):
super().__init__(user)
self.ip = ip
@@ -51,7 +69,6 @@ class OAuthBindMessage(UserMessage):
now = local_now_display()
subject = self.oauth_name + ' ' + _('binding reminder')
context = dict(
subject=subject,
name=self.user.name,
username=self.user.username,
ip=self.ip,
@@ -59,9 +76,9 @@ class OAuthBindMessage(UserMessage):
oauth_name=self.oauth_name,
oauth_id=self.oauth_id
)
message = render_to_string('authentication/_msg_oauth_bind.html', context)
message = custom_render_to_string(self.template_name, context)
return {
'subject': subject,
'subject': str(subject),
'message': message
}

View File

@@ -20,9 +20,10 @@ class UserConfirmation(permissions.BasePermission):
if not settings.SECURITY_VIEW_AUTH_NEED_MFA:
return True
confirm_level = request.session.get('CONFIRM_LEVEL')
confirm_type = request.session.get('CONFIRM_TYPE')
confirm_time = request.session.get('CONFIRM_TIME')
session = getattr(request, 'session', {})
confirm_level = session.get('CONFIRM_LEVEL')
confirm_type = session.get('CONFIRM_TYPE')
confirm_time = session.get('CONFIRM_TIME')
ttl = self.get_ttl(confirm_type)
now = int(time.time())

View File

@@ -3,7 +3,7 @@ from rest_framework import serializers
from accounts.const import SecretType
from accounts.models import Account
from acls.models import CommandGroup, CommandFilterACL
from acls.models import CommandGroup, CommandFilterACL, DataMaskingRule
from assets.models import Asset, Platform, Gateway, Zone
from assets.serializers.asset import AssetProtocolsSerializer
from assets.serializers.platform import PlatformSerializer
@@ -60,7 +60,7 @@ class _ConnectionTokenAccountSerializer(serializers.ModelSerializer):
]
@staticmethod
def get_su_from(account):
def get_su_from(account) -> dict:
if not hasattr(account, 'asset'):
return {}
su_enabled = account.asset.platform.su_enabled
@@ -83,6 +83,14 @@ class _ConnectionTokenGatewaySerializer(serializers.ModelSerializer):
]
class _ConnectionTokenDataMaskingRuleSerializer(serializers.ModelSerializer):
class Meta:
model = DataMaskingRule
fields = ['id', 'name', 'fields_pattern',
'masking_method', 'mask_pattern',
'is_active', 'priority']
class _ConnectionTokenCommandFilterACLSerializer(serializers.ModelSerializer):
command_groups = ObjectRelatedField(
many=True, required=False, queryset=CommandGroup.objects,
@@ -105,7 +113,7 @@ class _ConnectionTokenPlatformSerializer(PlatformSerializer):
class Meta(PlatformSerializer.Meta):
model = Platform
fields = [field for field in PlatformSerializer.Meta.fields
if field not in PlatformSerializer.Meta.fields_m2m]
if field not in PlatformSerializer.Meta.fields_m2m]
def get_field_names(self, declared_fields, info):
names = super().get_field_names(declared_fields, info)
@@ -139,6 +147,7 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
platform = _ConnectionTokenPlatformSerializer(read_only=True)
zone = ObjectRelatedField(queryset=Zone.objects, required=False, label=_('Domain'))
command_filter_acls = _ConnectionTokenCommandFilterACLSerializer(read_only=True, many=True)
data_masking_rules = _ConnectionTokenDataMaskingRuleSerializer(read_only=True, many=True)
expire_now = serializers.BooleanField(label=_('Expired now'), write_only=True, default=True)
connect_method = _ConnectTokenConnectMethodSerializer(read_only=True, source='connect_method_object')
connect_options = serializers.JSONField(read_only=True)
@@ -149,7 +158,7 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
model = ConnectionToken
fields = [
'id', 'value', 'user', 'asset', 'account',
'platform', 'command_filter_acls', 'protocol',
'platform', 'command_filter_acls', 'data_masking_rules', 'protocol',
'zone', 'gateway', 'actions', 'expire_at',
'from_ticket', 'expire_now', 'connect_method',
'connect_options', 'face_monitor_token'

View File

@@ -6,6 +6,7 @@ from common.serializers import CommonModelSerializer
from common.serializers.fields import EncryptedField
from perms.serializers.permission import ActionChoicesField
from ..models import ConnectionToken, AdminConnectionToken
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
__all__ = [
'ConnectionTokenSerializer', 'SuperConnectionTokenSerializer',
@@ -13,7 +14,7 @@ __all__ = [
]
class ConnectionTokenSerializer(CommonModelSerializer):
class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
expire_time = serializers.IntegerField(read_only=True, label=_('Expired time'))
input_secret = EncryptedField(
label=_("Input secret"), max_length=40960, required=False, allow_blank=True
@@ -60,7 +61,7 @@ class ConnectionTokenSerializer(CommonModelSerializer):
validated_data['remote_addr'] = get_request_ip(request)
return super().create(validated_data)
def get_from_ticket_info(self, instance):
def get_from_ticket_info(self, instance) -> dict:
if not instance.from_ticket:
return {}
user = self.get_request_user()

View File

@@ -46,7 +46,7 @@ class BearerTokenSerializer(serializers.Serializer):
user = UserProfileSerializer(read_only=True)
@staticmethod
def get_keyword(obj):
def get_keyword(obj) -> str:
return 'Bearer'
@staticmethod

View File

@@ -12,7 +12,7 @@
<td style="height: 50px;">{% trans 'MFA code' %}: <span style="font-weight: bold;">{{ code }}</span></td>
</tr>
<tr style="border: 1px solid #eee">
<td style="height: 30px;">{% trans 'The validity period of the verification code is one minute' %}</td>
<td style="height: 30px;">{{ tip }}</td>
</tr>
</table>
</div>

View File

@@ -11,8 +11,6 @@
<b>{% trans 'Time' %}:</b> {{ time }}<br>
<b>{% trans 'IP' %}:</b> {{ ip }}
</p>
-
<p>
{% trans 'If the operation is not your own, unbind and change the password.' %}
</p>

View File

@@ -6,12 +6,12 @@
{% trans 'Please click the link below to reset your password, if not your request, concern your account security' %}
<br>
<br>
<a href="{{ rest_password_url }}?token={{ rest_password_token}}" class='showLink' target="_blank">
<a href="{{ rest_password_url }}?token={{ rest_password_token }}" class='showLink' target="_blank">
{% 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>
<a href="{{ forget_password_url }}?email={{ email }}">{% trans 'request new one' %}</a>
</p>

View File

@@ -15,7 +15,7 @@
<td style="height: 30px;"> {% trans 'Copy the verification code to the Reset Password page to reset the password.' %} </td>
</tr>
<tr style="border: 1px solid #eee">
<td style="height: 30px;">{% trans 'The validity period of the verification code is one minute' %}</td>
<td style="height: 30px;">{{ tip }}</td>
</tr>
</table>
</div>

View File

@@ -15,7 +15,7 @@ from common.utils import get_logger
from common.utils.common import get_request_ip
from common.utils.django import reverse, get_object_or_none
from users.models import User
from users.signal_handlers import check_only_allow_exist_user_auth, bind_user_to_org_role
from users.signal_handlers import bind_user_to_org_role, check_only_allow_exist_user_auth
from .mixins import FlashMessageMixin
logger = get_logger(__file__)
@@ -55,7 +55,6 @@ class BaseLoginCallbackView(AuthMixin, FlashMessageMixin, IMClientMixin, View):
)
if not check_only_allow_exist_user_auth(create):
user.delete()
return user, (self.msg_client_err, self.request.error_message)
setattr(user, f'{self.user_type}_id', user_id)

View File

@@ -6,7 +6,7 @@ from __future__ import unicode_literals
import datetime
import os
from typing import Callable
from urllib.parse import urlparse
from urllib.parse import urlparse, urlsplit, urlunsplit, urlencode
from django.conf import settings
from django.contrib.auth import BACKEND_SESSION_KEY
@@ -155,9 +155,18 @@ class UserLoginView(mixins.AuthMixin, UserLoginContextMixin, FormView):
auth_name, redirect_url = auth_method['name'], auth_method['url']
next_url = request.GET.get('next') or '/'
next_url = safe_next_url(next_url, request=request)
query_string = request.GET.urlencode()
redirect_url = '{}?next={}&{}'.format(redirect_url, next_url, query_string)
merged_qs_items = dict(request.GET.lists())
merged_qs_items.pop('next', None)
merged = {}
for k, v_list in merged_qs_items.items():
merged[k] = v_list if len(v_list) > 1 else (v_list[0] if v_list else '')
merged['next'] = next_url
query = urlencode(merged, doseq=True)
u = urlsplit(redirect_url)
redirect_url = urlunsplit((u.scheme, u.netloc, u.path, query, u.fragment))
if settings.LOGIN_REDIRECT_MSG_ENABLED:
message_data = {
'title': _('Redirecting'),
@@ -165,7 +174,7 @@ class UserLoginView(mixins.AuthMixin, UserLoginContextMixin, FormView):
'redirect_url': redirect_url,
'interval': 3,
'has_cancel': True,
'cancel_url': reverse('authentication:login') + f'?admin=1&{query_string}'
'cancel_url': reverse('authentication:login') + f'?admin=1&{query}'
}
redirect_url = FlashMessageUtil.gen_message_url(message_data)
return redirect_url

View File

@@ -1,9 +1,11 @@
# -*- coding: utf-8 -*-
#
from django.conf import settings
from typing import Callable
from django.utils.translation import gettext as _
from rest_framework.decorators import action
from rest_framework.throttling import UserRateThrottle
from rest_framework.request import Request
from rest_framework.response import Response
@@ -14,8 +16,12 @@ from orgs.utils import current_org
__all__ = ['SuggestionMixin', 'RenderToJsonMixin']
class CustomUserRateThrottle(UserRateThrottle):
rate = '60/m'
class SuggestionMixin:
suggestion_limit = 10
suggestion_limit = settings.SUGGESTION_LIMIT
filter_queryset: Callable
get_queryset: Callable
@@ -35,6 +41,7 @@ class SuggestionMixin:
queryset = queryset.none()
queryset = self.filter_queryset(queryset)
queryset = queryset[:self.suggestion_limit]
page = self.paginate_queryset(queryset)
@@ -45,6 +52,11 @@ class SuggestionMixin:
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
def get_throttles(self):
if self.action == 'match':
return [CustomUserRateThrottle()]
return super().get_throttles()
class RenderToJsonMixin:
@action(methods=[POST, PUT], detail=False, url_path='render-to-json')

View File

@@ -5,6 +5,7 @@ from contextlib import nullcontext
from itertools import chain
from typing import Callable
from django.conf import settings
from django.db import models
from django.db.models.signals import m2m_changed
from rest_framework.request import Request
@@ -16,6 +17,7 @@ from common.drf.filters import (
IDNotFilterBackend, NotOrRelFilterBackend, LabelFilterBackend
)
from common.utils import get_logger, lazyproperty
from common.utils import is_uuid
from orgs.utils import tmp_to_org, tmp_to_root_org
from .action import RenderToJsonMixin
from .serializer import SerializerMixin
@@ -95,9 +97,33 @@ class QuerySetMixin:
request: Request
get_serializer_class: Callable
get_queryset: Callable
slug_field = 'name'
def get_queryset(self):
return super().get_queryset()
def get_object(self):
pk = self.kwargs.get(self.lookup_field)
if not pk or is_uuid(pk) or pk.isdigit():
return super().get_object()
return self.get_queryset().get(**{self.slug_field: pk})
def limit_queryset_if_no_page(self, queryset):
if self.request.query_params.get('format') in ['csv', 'xlsx']:
return queryset
action = getattr(self, 'action', None)
if action != 'list':
return queryset
# 如果分页器有设置 limit则不限制
if self.paginator and self.paginator.get_limit(self.request):
return queryset
# 如果分页器没有设置 limit则不限制
if getattr(self, 'page_no_limit', False):
return queryset
if not settings.DEFAULT_PAGE_SIZE:
return queryset
return queryset[:settings.DEFAULT_PAGE_SIZE]
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
@@ -106,6 +132,7 @@ class QuerySetMixin:
if self.action == 'metadata':
queryset = queryset.none()
queryset = self.setup_eager_loading(queryset)
queryset = self.limit_queryset_if_no_page(queryset)
return queryset
def setup_eager_loading(self, queryset, is_paginated=False):

View File

@@ -77,6 +77,7 @@ class Language(models.TextChoices):
es = 'es', 'Español'
ru = 'ru', 'Русский'
ko = 'ko', '한국어'
vi = 'vi', 'Tiếng Việt'
@classmethod
def get_code_mapper(cls):

View File

@@ -143,7 +143,9 @@ class EncryptMixin:
if value is None:
return value
plain_value = Encryptor(value).decrypt()
encryptor = Encryptor(value)
plain_value = encryptor.decrypt()
# 可能和Json mix所以要先解密再json
sp = super()
if hasattr(sp, "from_db_value"):
@@ -166,9 +168,6 @@ class EncryptMixin:
class EncryptTextField(EncryptMixin, models.TextField):
description = _("Encrypt field using Secret Key")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class EncryptCharField(EncryptMixin, models.CharField):
@staticmethod

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