diff --git a/.dockerignore b/.dockerignore index 81c9033ba..0ddcc29ae 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,4 @@ .git -logs/* data/* .github tmp/* diff --git a/.github/ISSUE_TEMPLATE/----.md b/.github/ISSUE_TEMPLATE/----.md index 147f42db4..47b19f79e 100644 --- a/.github/ISSUE_TEMPLATE/----.md +++ b/.github/ISSUE_TEMPLATE/----.md @@ -6,8 +6,7 @@ labels: 类型:需求 assignees: - ibuler - baijiangjie - - + - wojiushixiaobai --- **请描述您的需求或者改进建议.** diff --git a/.github/workflows/issue-comment.yml b/.github/workflows/issue-comment.yml index 5388111d7..980d701cb 100644 --- a/.github/workflows/issue-comment.yml +++ b/.github/workflows/issue-comment.yml @@ -21,17 +21,44 @@ jobs: actions: 'remove-labels' labels: '状态:待反馈' - add-label-if-not-author: + add-label-if-is-member: runs-on: ubuntu-latest - if: (github.event.issue.user.id != github.event.comment.user.id) && !github.event.issue.pull_request && (github.event.issue.state == 'open') steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Get Organization name + id: org_name + run: echo "data=$(echo '${{ github.repository }}' | cut -d '/' -f 1)" >> $GITHUB_OUTPUT + + - name: Get Organization public members + uses: octokit/request-action@v2.x + id: members + with: + route: GET /orgs/${{ steps.org_name.outputs.data }}/public_members + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Process public members data + # 将 members 中的数据转化为 login 字段的拼接字符串 + id: member_names + run: echo "data=$(echo '${{ steps.members.outputs.data }}' | jq '[.[].login] | join(",")')" >> $GITHUB_OUTPUT + + + - run: "echo members: '${{ steps.members.outputs.data }}'" + - run: "echo member names: '${{ steps.member_names.outputs.data }}'" + - run: "echo comment user: '${{ github.event.comment.user.login }}'" + - run: "echo contains? : '${{ contains(steps.member_names.outputs.data, github.event.comment.user.login) }}'" + - name: Add require replay label + if: contains(steps.member_names.outputs.data, github.event.comment.user.login) uses: actions-cool/issues-helper@v2 with: actions: 'add-labels' labels: '状态:待反馈' - name: Remove require handle label + if: contains(steps.member_names.outputs.data, github.event.comment.user.login) uses: actions-cool/issues-helper@v2 with: actions: 'remove-labels' diff --git a/.gitignore b/.gitignore index 9573a70b7..985f77580 100644 --- a/.gitignore +++ b/.gitignore @@ -35,7 +35,6 @@ celerybeat-schedule.db docs/_build/ xpack xpack.bak -logs/* ### Vagrant ### .vagrant/ release/* diff --git a/Dockerfile b/Dockerfile index 3dc78a9df..d8fd27492 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9-slim-buster as stage-build +FROM jumpserver/python:3.9-slim-buster as stage-build ARG TARGETARCH ARG VERSION @@ -8,7 +8,7 @@ WORKDIR /opt/jumpserver ADD . . RUN cd utils && bash -ixeu build.sh -FROM python:3.9-slim-buster +FROM jumpserver/python:3.9-slim-buster ARG TARGETARCH MAINTAINER JumpServer Team @@ -24,6 +24,7 @@ ARG DEPENDENCIES=" \ libjpeg-dev \ libldap2-dev \ libsasl2-dev \ + libssl-dev \ libxml2-dev \ libxmlsec1-dev \ libxmlsec1-openssl \ @@ -66,27 +67,36 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \ ARG DOWNLOAD_URL=https://download.jumpserver.org -RUN mkdir -p /opt/oracle/ \ - && cd /opt/oracle/ \ - && wget ${DOWNLOAD_URL}/public/instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip \ - && unzip instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip \ - && sh -c "echo /opt/oracle/instantclient_19_10 > /etc/ld.so.conf.d/oracle-instantclient.conf" \ - && ldconfig \ - && rm -f instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip +RUN set -ex \ + && \ + if [ "${TARGETARCH}" == "amd64" ] || [ "${TARGETARCH}" == "arm64" ]; then \ + mkdir -p /opt/oracle; \ + cd /opt/oracle; \ + wget ${DOWNLOAD_URL}/public/instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip; \ + unzip instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip; \ + echo "/opt/oracle/instantclient_19_10" > /etc/ld.so.conf.d/oracle-instantclient.conf; \ + ldconfig; \ + rm -f instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip; \ + fi WORKDIR /tmp/build COPY ./requirements ./requirements ARG PIP_MIRROR=https://pypi.douban.com/simple -ENV PIP_MIRROR=$PIP_MIRROR ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple -ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR RUN --mount=type=cache,target=/root/.cache/pip \ set -ex \ && pip config set global.index-url ${PIP_MIRROR} \ && pip install --upgrade pip \ && pip install --upgrade setuptools wheel \ + && \ + if [ "${TARGETARCH}" == "loong64" ]; then \ + pip install https://download.jumpserver.org/pypi/simple/cryptography/cryptography-38.0.4-cp39-cp39-linux_loongarch64.whl; \ + pip install https://download.jumpserver.org/pypi/simple/greenlet/greenlet-1.1.2-cp39-cp39-linux_loongarch64.whl; \ + pip install https://download.jumpserver.org/pypi/simple/PyNaCl/PyNaCl-1.5.0-cp39-cp39-linux_loongarch64.whl; \ + pip install https://download.jumpserver.org/pypi/simple/grpcio/grpcio-1.54.2-cp39-cp39-linux_loongarch64.whl; \ + fi \ && pip install $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \ && pip install -r requirements/requirements.txt diff --git a/Dockerfile.loong64 b/Dockerfile.loong64 deleted file mode 100644 index f5682aec2..000000000 --- a/Dockerfile.loong64 +++ /dev/null @@ -1,97 +0,0 @@ -FROM python:3.9-slim-buster as stage-build -ARG TARGETARCH - -ARG VERSION -ENV VERSION=$VERSION - -WORKDIR /opt/jumpserver -ADD . . -RUN cd utils && bash -ixeu build.sh - -FROM python:3.9-slim-buster -ARG TARGETARCH -MAINTAINER JumpServer Team - -ARG BUILD_DEPENDENCIES=" \ - g++ \ - make \ - pkg-config" - -ARG DEPENDENCIES=" \ - freetds-dev \ - libpq-dev \ - libffi-dev \ - libjpeg-dev \ - libldap2-dev \ - libsasl2-dev \ - libssl-dev \ - libxml2-dev \ - libxmlsec1-dev \ - libxmlsec1-openssl \ - freerdp2-dev \ - libaio-dev" - -ARG TOOLS=" \ - ca-certificates \ - curl \ - default-libmysqlclient-dev \ - default-mysql-client \ - locales \ - openssh-client \ - procps \ - sshpass \ - telnet \ - unzip \ - vim \ - git \ - wget" - -RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \ - set -ex \ - && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ - && apt-get update \ - && apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \ - && apt-get -y install --no-install-recommends ${DEPENDENCIES} \ - && apt-get -y install --no-install-recommends ${TOOLS} \ - && mkdir -p /root/.ssh/ \ - && echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \ - && echo "set mouse-=a" > ~/.vimrc \ - && echo "no" | dpkg-reconfigure dash \ - && echo "zh_CN.UTF-8" | dpkg-reconfigure locales \ - && sed -i "s@# export @export @g" ~/.bashrc \ - && sed -i "s@# alias @alias @g" ~/.bashrc \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /tmp/build -COPY ./requirements ./requirements - -ARG PIP_MIRROR=https://pypi.douban.com/simple -ENV PIP_MIRROR=$PIP_MIRROR -ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple -ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR - -RUN --mount=type=cache,target=/root/.cache/pip \ - set -ex \ - && pip config set global.index-url ${PIP_MIRROR} \ - && pip install --upgrade pip \ - && pip install --upgrade setuptools wheel \ - && pip install https://download.jumpserver.org/pypi/simple/cryptography/cryptography-38.0.4-cp39-cp39-linux_loongarch64.whl \ - && pip install https://download.jumpserver.org/pypi/simple/greenlet/greenlet-1.1.2-cp39-cp39-linux_loongarch64.whl \ - && pip install https://download.jumpserver.org/pypi/simple/PyNaCl/PyNaCl-1.5.0-cp39-cp39-linux_loongarch64.whl \ - && pip install https://download.jumpserver.org/pypi/simple/grpcio/grpcio-1.54.2-cp39-cp39-linux_loongarch64.whl \ - && pip install $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \ - && pip install -r requirements/requirements.txt - -COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver -RUN echo > /opt/jumpserver/config.yml \ - && rm -rf /tmp/build - -WORKDIR /opt/jumpserver -VOLUME /opt/jumpserver/data -VOLUME /opt/jumpserver/logs - -ENV LANG=zh_CN.UTF-8 - -EXPOSE 8080 - -ENTRYPOINT ["./entrypoint.sh"] diff --git a/README.md b/README.md index 71872e4a2..634a8e56a 100644 --- a/README.md +++ b/README.md @@ -17,18 +17,16 @@ 9 年时间,倾情投入,用心做好一款开源堡垒机。

-| :warning: 注意 :warning: | -|:-------------------------------------------------------------------------------------------------------------------------:| -| 3.0 架构上和 2.0 变化较大,建议全新安装一套环境来体验。如需升级,请务必升级前进行备份,并[查阅文档](https://kb.fit2cloud.com/?p=06638d69-f109-4333-b5bf-65b17b297ed9) | +JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。 --------------------------- - -JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型的资产,包括: +JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型的资产,包括: - **SSH**: Linux / Unix / 网络设备 等; - **Windows**: Web 方式连接 / 原生 RDP 连接; -- **数据库**: MySQL / Oracle / SQLServer / PostgreSQL 等; -- **Kubernetes**: 支持连接到 K8s 集群中的 Pods; +- **数据库**: MySQL / MariaDB / PostgreSQL / Oracle / SQLServer / ClickHouse 等; +- **NoSQL**: Redis / MongoDB 等; +- **GPT**: ChatGPT 等; +- **云服务**: Kubernetes / VMware vSphere 等; - **Web 站点**: 各类系统的 Web 管理后台; - **应用**: 通过 Remote App 连接各类应用。 @@ -81,11 +79,7 @@ JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运 如果您在使用过程中有任何疑问或对建议,欢迎提交 [GitHub Issue](https://github.com/jumpserver/jumpserver/issues/new/choose)。 -您也可以到我们的 [社区论坛](https://bbs.fit2cloud.com/c/js/5) 及微信交流群当中进行交流沟通。 - -**微信交流群** - -微信群二维码 +您也可以到我们的 [社区论坛](https://bbs.fit2cloud.com/c/js/5) 当中进行交流沟通。 ### 参与贡献 @@ -95,15 +89,20 @@ JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运 ## 组件项目 -| 项目 | 状态 | 描述 | -|--------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------| -| [Lina](https://github.com/jumpserver/lina) | Lina release | JumpServer Web UI 项目 | -| [Luna](https://github.com/jumpserver/luna) | Luna release | JumpServer Web Terminal 项目 | -| [KoKo](https://github.com/jumpserver/koko) | Koko release | JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco) | -| [Lion](https://github.com/jumpserver/lion-release) | Lion release | JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/) | -| [Magnus](https://github.com/jumpserver/magnus-release) | Magnus release | JumpServer 数据库代理 Connector 项目 | -| [Clients](https://github.com/jumpserver/clients) | Clients release | JumpServer 客户端 项目 | -| [Installer](https://github.com/jumpserver/installer) | Installer release | JumpServer 安装包 项目 | +| 项目 | 状态 | 描述 | +|--------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------| +| [Lina](https://github.com/jumpserver/lina) | Lina release | JumpServer Web UI 项目 | +| [Luna](https://github.com/jumpserver/luna) | Luna release | JumpServer Web Terminal 项目 | +| [KoKo](https://github.com/jumpserver/koko) | Koko release | JumpServer 字符协议 Connector 项目 | +| [Lion](https://github.com/jumpserver/lion-release) | Lion release | JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/) | +| [Razor](https://github.com/jumpserver/razor) | Chen | JumpServer RDP 代理 Connector 项目 | +| [Tinker](https://github.com/jumpserver/tinker) | Tinker | JumpServer 远程应用 Connector 项目 | +| [Magnus](https://github.com/jumpserver/magnus-release) | Magnus release | JumpServer 数据库代理 Connector 项目 | +| [Chen](https://github.com/jumpserver/chen-release) | Chen release | JumpServer Web DB 项目,替代原来的 OmniDB | +| [Kael](https://github.com/jumpserver/kael) | Kael release | JumpServer 连接 GPT 资产的组件项目 | +| [Wisp](https://github.com/jumpserver/wisp) | Magnus release | JumpServer 各系统终端组件和 Core Api 通信的组件项目 | +| [Clients](https://github.com/jumpserver/clients) | Clients release | JumpServer 客户端 项目 | +| [Installer](https://github.com/jumpserver/installer) | Installer release | JumpServer 安装包 项目 | ## 安全说明 @@ -113,11 +112,6 @@ JumpServer是一款安全产品,请参考 [基本安全建议](https://docs.ju - 邮箱:support@fit2cloud.com - 电话:400-052-0755 -## 致谢开源 - -- [Apache Guacamole](https://guacamole.apache.org/): Web 页面连接 RDP、SSH、VNC 等协议资产,JumpServer Lion 组件使用到该项目; -- [OmniDB](https://omnidb.org/): Web 页面连接使用数据库,JumpServer Web 数据库组件使用到该项目。 - ## License & Copyright Copyright (c) 2014-2023 飞致云 FIT2CLOUD, All rights reserved. diff --git a/apps/accounts/api/account/template.py b/apps/accounts/api/account/template.py index 0aecb5143..1d2508764 100644 --- a/apps/accounts/api/account/template.py +++ b/apps/accounts/api/account/template.py @@ -49,8 +49,7 @@ class AccountTemplateViewSet(OrgBulkModelViewSet): @action(methods=['get'], detail=False, url_path='su-from-account-templates') def su_from_account_templates(self, request, *args, **kwargs): pk = request.query_params.get('template_id') - template = AccountTemplate.objects.filter(pk=pk).first() - templates = AccountTemplate.get_su_from_account_templates(template) + templates = AccountTemplate.get_su_from_account_templates(pk) templates = self.filter_queryset(templates) serializer = self.get_serializer(templates, many=True) return Response(data=serializer.data) diff --git a/apps/accounts/automations/change_secret/host/aix/manifest.yml b/apps/accounts/automations/change_secret/host/aix/manifest.yml index e84a9b42b..a3df14a4d 100644 --- a/apps/accounts/automations/change_secret/host/aix/manifest.yml +++ b/apps/accounts/automations/change_secret/host/aix/manifest.yml @@ -4,9 +4,58 @@ category: host type: - AIX method: change_secret +params: + - name: sudo + type: str + label: 'Sudo' + default: '/bin/whoami' + help_text: "{{ 'Params sudo help text' | trans }}" + + - name: shell + type: str + label: 'Shell' + default: '/bin/bash' + + - name: home + type: str + label: "{{ 'Params home label' | trans }}" + default: '' + help_text: "{{ 'Params home help text' | trans }}" + + - name: groups + type: str + label: "{{ 'Params groups label' | trans }}" + default: '' + help_text: "{{ 'Params groups help text' | trans }}" i18n: AIX account change secret: - zh: 使用 Ansible 模块 user 执行账号改密 (DES) - ja: Ansible user モジュールを使用してアカウントのパスワード変更 (DES) - en: Using Ansible module user to change account secret (DES) + zh: '使用 Ansible 模块 user 执行账号改密 (DES)' + ja: 'Ansible user モジュールを使用してアカウントのパスワード変更 (DES)' + en: 'Using Ansible module user to change account secret (DES)' + + Params sudo help text: + zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig' + ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig' + en: 'Use commas to separate multiple commands, such as: /bin/whoami,/sbin/ifconfig' + + Params home help text: + zh: '默认家目录 /home/{账号用户名}' + ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}' + en: 'Default home directory /home/{account username}' + + Params groups help text: + zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' + ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' + en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)' + + Params home label: + zh: '家目录' + ja: 'ホームディレクトリ' + en: 'Home' + + Params groups label: + zh: '用户组' + ja: 'グループ' + en: 'Groups' + diff --git a/apps/accounts/automations/change_secret/host/posix/main.yml b/apps/accounts/automations/change_secret/host/posix/main.yml index 80f0aa01c..325ad644d 100644 --- a/apps/accounts/automations/change_secret/host/posix/main.yml +++ b/apps/accounts/automations/change_secret/host/posix/main.yml @@ -4,6 +4,26 @@ - name: Test privileged account ansible.builtin.ping: + - name: Check user + ansible.builtin.user: + name: "{{ account.username }}" + shell: "{{ params.shell }}" + home: "{{ params.home | default('/home/' + account.username, true) }}" + groups: "{{ params.groups }}" + expires: -1 + state: present + + - name: "Add {{ account.username }} group" + ansible.builtin.group: + name: "{{ account.username }}" + state: present + + - name: Add user groups + ansible.builtin.user: + name: "{{ account.username }}" + groups: "{{ params.groups }}" + when: params.groups + - name: Change password ansible.builtin.user: name: "{{ account.username }}" @@ -33,6 +53,16 @@ exclusive: "{{ ssh_params.exclusive }}" when: account.secret_type == "ssh_key" + - name: Set sudo setting + ansible.builtin.lineinfile: + dest: /etc/sudoers + state: present + regexp: "^{{ account.username }} ALL=" + line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}" + validate: visudo -cf %s + when: + - params.sudo + - name: Refresh connection ansible.builtin.meta: reset_connection diff --git a/apps/accounts/automations/change_secret/host/posix/manifest.yml b/apps/accounts/automations/change_secret/host/posix/manifest.yml index 6aa1bba10..43d1ca5fd 100644 --- a/apps/accounts/automations/change_secret/host/posix/manifest.yml +++ b/apps/accounts/automations/change_secret/host/posix/manifest.yml @@ -5,9 +5,59 @@ type: - unix - linux method: change_secret +params: + - name: sudo + type: str + label: 'Sudo' + default: '/bin/whoami' + help_text: "{{ 'Params sudo help text' | trans }}" + + - name: shell + type: str + label: 'Shell' + default: '/bin/bash' + help_text: '' + + - name: home + type: str + label: "{{ 'Params home label' | trans }}" + default: '' + help_text: "{{ 'Params home help text' | trans }}" + + - name: groups + type: str + label: "{{ 'Params groups label' | trans }}" + default: '' + help_text: "{{ 'Params groups help text' | trans }}" i18n: Posix account change secret: - zh: 使用 Ansible 模块 user 执行账号改密 (SHA512) - ja: Ansible user モジュールを使用して アカウントのパスワード変更 (SHA512) - en: Using Ansible module user to change account secret (SHA512) + zh: '使用 Ansible 模块 user 执行账号改密 (SHA512)' + ja: 'Ansible user モジュールを使用して アカウントのパスワード変更 (SHA512)' + en: 'Using Ansible module user to change account secret (SHA512)' + + Params sudo help text: + zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig' + ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig' + en: 'Use commas to separate multiple commands, such as: /bin/whoami,/sbin/ifconfig' + + Params home help text: + zh: '默认家目录 /home/{账号用户名}' + ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}' + en: 'Default home directory /home/{account username}' + + Params groups help text: + zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' + ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' + en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)' + + Params home label: + zh: '家目录' + ja: 'ホームディレクトリ' + en: 'Home' + + Params groups label: + zh: '用户组' + ja: 'グループ' + en: 'Groups' + diff --git a/apps/accounts/automations/change_secret/host/windows/main.yml b/apps/accounts/automations/change_secret/host/windows/main.yml index 86ea7a81f..a97166fef 100644 --- a/apps/accounts/automations/change_secret/host/windows/main.yml +++ b/apps/accounts/automations/change_secret/host/windows/main.yml @@ -8,17 +8,13 @@ # debug: # msg: "Username: {{ account.username }}, Password: {{ account.secret }}" - - - name: Get groups of a Windows user - ansible.windows.win_user: - name: "{{ jms_account.username }}" - register: user_info - - name: Change password ansible.windows.win_user: + fullname: "{{ account.username}}" name: "{{ account.username }}" password: "{{ account.secret }}" - groups: "{{ user_info.groups[0].name }}" + password_never_expires: yes + groups: "{{ params.groups }}" groups_action: add update_password: always ignore_errors: true diff --git a/apps/accounts/automations/change_secret/host/windows/manifest.yml b/apps/accounts/automations/change_secret/host/windows/manifest.yml index 4480a9ecf..884168974 100644 --- a/apps/accounts/automations/change_secret/host/windows/manifest.yml +++ b/apps/accounts/automations/change_secret/host/windows/manifest.yml @@ -5,9 +5,22 @@ method: change_secret category: host type: - windows +params: + - name: groups + type: str + label: '用户组' + default: 'Users,Remote Desktop Users' + help_text: "{{ 'Params groups help text' | trans }}" + i18n: Windows account change secret: - zh: 使用 Ansible 模块 win_user 执行 Windows 账号改密 - ja: Ansible win_user モジュールを使用して Windows アカウントのパスワード変更 - en: Using Ansible module win_user to change Windows account secret + zh: '使用 Ansible 模块 win_user 执行 Windows 账号改密' + ja: 'Ansible win_user モジュールを使用して Windows アカウントのパスワード変更' + en: 'Using Ansible module win_user to change Windows account secret' + + Params groups help text: + zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' + ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' + en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)' + diff --git a/apps/accounts/automations/verify_account/custom/rdp/main.yml b/apps/accounts/automations/verify_account/custom/rdp/main.yml index cf4a937a7..b0c7cbe4f 100644 --- a/apps/accounts/automations/verify_account/custom/rdp/main.yml +++ b/apps/accounts/automations/verify_account/custom/rdp/main.yml @@ -1,11 +1,12 @@ - hosts: custom gather_facts: no vars: + ansible_shell_type: sh ansible_connection: local tasks: - - name: Verify account - ssh_ping: + - name: Verify account (pyfreerdp) + rdp_ping: login_host: "{{ jms_asset.address }}" login_port: "{{ jms_asset.port }}" login_user: "{{ account.username }}" diff --git a/apps/accounts/automations/verify_account/custom/ssh/main.yml b/apps/accounts/automations/verify_account/custom/ssh/main.yml index cf4a937a7..29b1dc22b 100644 --- a/apps/accounts/automations/verify_account/custom/ssh/main.yml +++ b/apps/accounts/automations/verify_account/custom/ssh/main.yml @@ -4,7 +4,7 @@ ansible_connection: local tasks: - - name: Verify account + - name: Verify account (paramiko) ssh_ping: login_host: "{{ jms_asset.address }}" login_port: "{{ jms_asset.port }}" diff --git a/apps/accounts/const/account.py b/apps/accounts/const/account.py index 55fa02d80..9a3541001 100644 --- a/apps/accounts/const/account.py +++ b/apps/accounts/const/account.py @@ -7,12 +7,14 @@ class SecretType(TextChoices): SSH_KEY = 'ssh_key', _('SSH key') ACCESS_KEY = 'access_key', _('Access key') TOKEN = 'token', _('Token') + API_KEY = 'api_key', _("API key") class AliasAccount(TextChoices): ALL = '@ALL', _('All') INPUT = '@INPUT', _('Manual input') USER = '@USER', _('Dynamic user') + ANON = '@ANON', _('Anonymous account') class Source(TextChoices): diff --git a/apps/accounts/filters.py b/apps/accounts/filters.py index 67e243e1c..be2cf1dfd 100644 --- a/apps/accounts/filters.py +++ b/apps/accounts/filters.py @@ -45,7 +45,7 @@ class AccountFilterSet(BaseFilterSet): class Meta: model = Account - fields = ['id', 'asset_id', 'source_id'] + fields = ['id', 'asset_id', 'source_id', 'secret_type'] class GatheredAccountFilterSet(BaseFilterSet): diff --git a/apps/accounts/migrations/0001_initial.py b/apps/accounts/migrations/0001_initial.py index b8fe35670..97ae584c4 100644 --- a/apps/accounts/migrations/0001_initial.py +++ b/apps/accounts/migrations/0001_initial.py @@ -1,12 +1,14 @@ # Generated by Django 3.2.14 on 2022-12-28 07:29 +import uuid + +import django.db.models.deletion +import simple_history.models +from django.conf import settings +from django.db import migrations, models + import common.db.encoder import common.db.fields -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import simple_history.models -import uuid class Migration(migrations.Migration): @@ -29,13 +31,16 @@ class Migration(migrations.Migration): ('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')), - ('connectivity', models.CharField(choices=[('-', 'Unknown'), ('ok', 'Ok'), ('err', 'Error')], default='-', max_length=16, verbose_name='Connectivity')), + ('connectivity', + models.CharField(choices=[('-', 'Unknown'), ('ok', 'Ok'), ('err', 'Error')], default='-', + max_length=16, verbose_name='Connectivity')), ('date_verified', models.DateTimeField(null=True, verbose_name='Date verified')), ('name', models.CharField(max_length=128, verbose_name='Name')), ('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')), ('secret_type', models.CharField( choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), - ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), + ('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16, + verbose_name='Secret type')), ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), ('privileged', models.BooleanField(default=False, verbose_name='Privileged')), ('is_active', models.BooleanField(default=True, verbose_name='Is active')), @@ -61,7 +66,8 @@ class Migration(migrations.Migration): ('id', models.UUIDField(db_index=True, default=uuid.uuid4)), ('secret_type', models.CharField( choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), - ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), + ('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16, + verbose_name='Secret type')), ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), ('version', models.IntegerField(default=0, verbose_name='Version')), ('history_id', models.AutoField(primary_key=True, serialize=False)), @@ -96,7 +102,8 @@ class Migration(migrations.Migration): ('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')), ('secret_type', models.CharField( choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), - ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), + ('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16, + verbose_name='Secret type')), ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), ('privileged', models.BooleanField(default=False, verbose_name='Privileged')), ('is_active', models.BooleanField(default=True, verbose_name='Is active')), diff --git a/apps/accounts/migrations/0003_automation.py b/apps/accounts/migrations/0003_automation.py index 503c766af..a341e18b2 100644 --- a/apps/accounts/migrations/0003_automation.py +++ b/apps/accounts/migrations/0003_automation.py @@ -1,11 +1,13 @@ # Generated by Django 3.2.16 on 2022-12-30 08:08 +import uuid + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + import common.db.encoder import common.db.fields -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import uuid class Migration(migrations.Migration): @@ -53,7 +55,8 @@ class Migration(migrations.Migration): primary_key=True, serialize=False, to='assets.baseautomation')), ('secret_type', models.CharField( choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), - ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), + ('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16, + verbose_name='Secret type')), ('secret_strategy', models.CharField(choices=[('specific', 'Specific password'), ('random_one', 'All assets use the same random password'), ('random_all', @@ -156,7 +159,8 @@ class Migration(migrations.Migration): primary_key=True, serialize=False, to='assets.baseautomation')), ('secret_type', models.CharField( choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), - ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), + ('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16, + verbose_name='Secret type')), ('secret_strategy', models.CharField(choices=[('specific', 'Specific password'), ('random_one', 'All assets use the same random password'), ('random_all', diff --git a/apps/accounts/models/__init__.py b/apps/accounts/models/__init__.py index c40ee786d..df686a50b 100644 --- a/apps/accounts/models/__init__.py +++ b/apps/accounts/models/__init__.py @@ -1,3 +1,3 @@ -from .base import * from .account import * from .automations import * +from .base import * diff --git a/apps/accounts/models/account.py b/apps/accounts/models/account.py index 30eb853e3..d84978b17 100644 --- a/apps/accounts/models/account.py +++ b/apps/accounts/models/account.py @@ -88,20 +88,33 @@ class Account(AbsConnectivity, BaseAccount): def has_secret(self): return bool(self.secret) + @classmethod + def get_special_account(cls, name): + if name == AliasAccount.INPUT.value: + return cls.get_manual_account() + elif name == AliasAccount.ANON.value: + return cls.get_anonymous_account() + else: + return cls(name=name, username=name, secret=None) + @classmethod def get_manual_account(cls): """ @INPUT 手动登录的账号(any) """ return cls(name=AliasAccount.INPUT.label, username=AliasAccount.INPUT.value, secret=None) - @lazyproperty - def versions(self): - return self.history.count() + @classmethod + def get_anonymous_account(cls): + return cls(name=AliasAccount.ANON.label, username=AliasAccount.ANON.value, secret=None) @classmethod def get_user_account(cls): """ @USER 动态用户的账号(self) """ return cls(name=AliasAccount.USER.label, username=AliasAccount.USER.value, secret=None) + @lazyproperty + def versions(self): + return self.history.count() + def get_su_from_accounts(self): """ 排除自己和以自己为 su-from 的账号 """ return self.asset.accounts.exclude(id=self.id).exclude(su_from=self) @@ -124,10 +137,13 @@ class AccountTemplate(BaseAccount): ] @classmethod - def get_su_from_account_templates(cls, instance=None): - if not instance: + def get_su_from_account_templates(cls, pk=None): + if pk is None: return cls.objects.all() - return cls.objects.exclude(Q(id=instance.id) | Q(su_from=instance)) + return cls.objects.exclude(Q(id=pk) | Q(su_from_id=pk)) + + def __str__(self): + return f'{self.name}({self.username})' def get_su_from_account(self, asset): su_from = self.su_from diff --git a/apps/accounts/serializers/account/base.py b/apps/accounts/serializers/account/base.py index b79dd51be..2f9660bd5 100644 --- a/apps/accounts/serializers/account/base.py +++ b/apps/accounts/serializers/account/base.py @@ -78,5 +78,8 @@ class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer): ] extra_kwargs = { 'spec_info': {'label': _('Spec info')}, - 'username': {'help_text': _("Tip: If no username is required for authentication, fill in `null`")} + 'username': {'help_text': _( + "Tip: If no username is required for authentication, fill in `null`, " + "If AD account, like `username@domain`" + )}, } diff --git a/apps/accounts/serializers/automations/base.py b/apps/accounts/serializers/automations/base.py index 1468ecf58..8e7f11f23 100644 --- a/apps/accounts/serializers/automations/base.py +++ b/apps/accounts/serializers/automations/base.py @@ -63,15 +63,17 @@ class AutomationExecutionSerializer(serializers.ModelSerializer): @staticmethod def get_snapshot(obj): - tp = obj.snapshot['type'] + tp = obj.snapshot.get('type', '') + type_display = tp if not hasattr(AutomationTypes, tp) \ + else getattr(AutomationTypes, tp).label snapshot = { 'type': tp, - 'name': obj.snapshot['name'], - 'comment': obj.snapshot['comment'], - 'accounts': obj.snapshot['accounts'], - 'node_amount': len(obj.snapshot['nodes']), - 'asset_amount': len(obj.snapshot['assets']), - 'type_display': getattr(AutomationTypes, tp).label, + 'name': obj.snapshot.get('name'), + 'comment': obj.snapshot.get('comment'), + 'accounts': obj.snapshot.get('accounts'), + 'node_amount': len(obj.snapshot.get('nodes', [])), + 'asset_amount': len(obj.snapshot.get('assets', [])), + 'type_display': type_display, } return snapshot diff --git a/apps/accounts/serializers/automations/change_secret.py b/apps/accounts/serializers/automations/change_secret.py index 94a7dc428..3c6e11205 100644 --- a/apps/accounts/serializers/automations/change_secret.py +++ b/apps/accounts/serializers/automations/change_secret.py @@ -50,7 +50,7 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ read_only_fields = BaseAutomationSerializer.Meta.read_only_fields fields = BaseAutomationSerializer.Meta.fields + read_only_fields + [ 'secret_type', 'secret_strategy', 'secret', 'password_rules', - 'ssh_key_change_strategy', 'passphrase', 'recipients', + 'ssh_key_change_strategy', 'passphrase', 'recipients', 'params' ] extra_kwargs = {**BaseAutomationSerializer.Meta.extra_kwargs, **{ 'accounts': {'required': True}, diff --git a/apps/accounts/serializers/automations/push_account.py b/apps/accounts/serializers/automations/push_account.py index 1d7bb3d36..baf1994a4 100644 --- a/apps/accounts/serializers/automations/push_account.py +++ b/apps/accounts/serializers/automations/push_account.py @@ -10,7 +10,7 @@ class PushAccountAutomationSerializer(ChangeSecretAutomationSerializer): class Meta(ChangeSecretAutomationSerializer.Meta): model = PushAccountAutomation - fields = ['params'] + [ + fields = [ n for n in ChangeSecretAutomationSerializer.Meta.fields if n not in ['recipients'] ] diff --git a/apps/accounts/urls.py b/apps/accounts/urls.py index 5c57ad67b..94c8311e0 100644 --- a/apps/accounts/urls.py +++ b/apps/accounts/urls.py @@ -39,7 +39,7 @@ urlpatterns = [ path('push-account//asset/remove/', api.PushAccountRemoveAssetApi.as_view(), name='push-account-remove-asset'), - path('push-accountt//asset/add/', api.PushAccountAddAssetApi.as_view(), name='push-account-add-asset'), + path('push-account//asset/add/', api.PushAccountAddAssetApi.as_view(), name='push-account-add-asset'), path('push-account//nodes/', api.PushAccountNodeAddRemoveApi.as_view(), name='push-account-add-or-remove-node'), path('push-account//assets/', api.PushAccountAssetsListApi.as_view(), name='push-account-assets'), diff --git a/apps/accounts/utils.py b/apps/accounts/utils.py index fc13a62d4..ef0d61fe1 100644 --- a/apps/accounts/utils.py +++ b/apps/accounts/utils.py @@ -4,7 +4,7 @@ from rest_framework import serializers from accounts.const import ( SecretType, DEFAULT_PASSWORD_RULES ) -from common.utils import gen_key_pair, random_string +from common.utils import ssh_key_gen, random_string from common.utils import validate_ssh_private_key, parse_ssh_private_key_str @@ -16,7 +16,7 @@ class SecretGenerator: @staticmethod def generate_ssh_key(): - private_key, public_key = gen_key_pair() + private_key, public_key = ssh_key_gen() return private_key def generate_password(self): diff --git a/apps/acls/const.py b/apps/acls/const.py new file mode 100644 index 000000000..c2d2be586 --- /dev/null +++ b/apps/acls/const.py @@ -0,0 +1,9 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +class ActionChoices(models.TextChoices): + reject = 'reject', _('Reject') + accept = 'accept', _('Accept') + review = 'review', _('Review') + warning = 'warning', _('Warning') diff --git a/apps/acls/migrations/0016_auto_20230606_1857.py b/apps/acls/migrations/0016_auto_20230606_1857.py index 2838b5d1f..d703dac52 100644 --- a/apps/acls/migrations/0016_auto_20230606_1857.py +++ b/apps/acls/migrations/0016_auto_20230606_1857.py @@ -1,5 +1,4 @@ # Generated by Django 3.2.17 on 2023-06-06 10:57 -from collections import defaultdict from django.db import migrations, models @@ -8,17 +7,20 @@ import common.db.fields def migrate_users_login_acls(apps, schema_editor): login_acl_model = apps.get_model('acls', 'LoginACL') - name_used = defaultdict(int) - for login_acl in login_acl_model.objects.all(): - name = login_acl.name - if name_used[name] > 0: - login_acl.name += "_{}".format(name_used[name]) - name_used[name] += 1 + name_used = [] + login_acls = [] + for login_acl in login_acl_model.objects.all().select_related('user'): + name = '{}_{}'.format(login_acl.name, login_acl.user.username) + if name.lower() in name_used: + name += '_{}'.format(str(login_acl.user_id)[:4]) + name_used.append(name.lower()) + login_acl.name = name login_acl.users = { "type": "ids", "ids": [str(login_acl.user_id)] } - login_acl.save() + login_acls.append(login_acl) + login_acl_model.objects.bulk_update(login_acls, ['name', 'users']) class Migration(migrations.Migration): diff --git a/apps/acls/models/base.py b/apps/acls/models/base.py index cbc5c6e4e..74782b2a3 100644 --- a/apps/acls/models/base.py +++ b/apps/acls/models/base.py @@ -7,6 +7,7 @@ from common.db.models import JMSBaseModel from common.utils import contains_ip from common.utils.time_period import contains_time_period from orgs.mixins.models import OrgModelMixin, OrgManager +from ..const import ActionChoices __all__ = [ 'BaseACL', 'UserBaseACL', 'UserAssetAccountBaseACL', @@ -16,12 +17,6 @@ from orgs.utils import tmp_to_root_org from orgs.utils import tmp_to_org -class ActionChoices(models.TextChoices): - reject = 'reject', _('Reject') - accept = 'accept', _('Accept') - review = 'review', _('Review') - - class BaseACLQuerySet(models.QuerySet): def active(self): return self.filter(is_active=True) diff --git a/apps/acls/serializers/base.py b/apps/acls/serializers/base.py index d8a172c93..94c4d9fa1 100644 --- a/apps/acls/serializers/base.py +++ b/apps/acls/serializers/base.py @@ -1,10 +1,11 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from acls.models.base import ActionChoices, BaseACL +from acls.models.base import BaseACL from common.serializers.fields import JSONManyToManyField, LabeledChoiceField from jumpserver.utils import has_valid_xpack_license from orgs.models import Organization +from ..const import ActionChoices common_help_text = _( "With * indicating a match all. " @@ -60,18 +61,21 @@ class ActionAclSerializer(serializers.Serializer): super().__init__(*args, **kwargs) self.set_action_choices() - def set_action_choices(self): - action = self.fields.get("action") - if not action: - return - choices = action.choices - if not has_valid_xpack_license(): - choices.pop(ActionChoices.review, None) - action._choices = choices - - -class BaserACLSerializer(ActionAclSerializer, serializers.Serializer): class Meta: + action_choices_exclude = [ActionChoices.warning] + + def set_action_choices(self): + field_action = self.fields.get("action") + if not field_action: + return + if not has_valid_xpack_license(): + field_action._choices.pop(ActionChoices.review, None) + for choice in self.Meta.action_choices_exclude: + field_action._choices.pop(choice, None) + + +class BaseACLSerializer(ActionAclSerializer, serializers.Serializer): + class Meta(ActionAclSerializer.Meta): model = BaseACL fields_mini = ["id", "name"] fields_small = fields_mini + [ @@ -84,6 +88,7 @@ class BaserACLSerializer(ActionAclSerializer, serializers.Serializer): extra_kwargs = { "priority": {"default": 50}, "is_active": {"default": True}, + 'reviewers': {'label': _('Recipients')}, } def validate_reviewers(self, reviewers): @@ -107,16 +112,16 @@ class BaserACLSerializer(ActionAclSerializer, serializers.Serializer): return valid_reviewers -class BaserUserACLSerializer(BaserACLSerializer): +class BaseUserACLSerializer(BaseACLSerializer): users = JSONManyToManyField(label=_('User')) - class Meta(BaserACLSerializer.Meta): - fields = BaserACLSerializer.Meta.fields + ['users'] + class Meta(BaseACLSerializer.Meta): + fields = BaseACLSerializer.Meta.fields + ['users'] -class BaseUserAssetAccountACLSerializer(BaserUserACLSerializer): +class BaseUserAssetAccountACLSerializer(BaseUserACLSerializer): assets = JSONManyToManyField(label=_('Asset')) accounts = serializers.ListField(label=_('Account')) - class Meta(BaserUserACLSerializer.Meta): - fields = BaserUserACLSerializer.Meta.fields + ['assets', 'accounts'] + class Meta(BaseUserACLSerializer.Meta): + fields = BaseUserACLSerializer.Meta.fields + ['assets', 'accounts'] diff --git a/apps/acls/serializers/command_acl.py b/apps/acls/serializers/command_acl.py index a34ea4cc4..672164012 100644 --- a/apps/acls/serializers/command_acl.py +++ b/apps/acls/serializers/command_acl.py @@ -31,6 +31,8 @@ class CommandFilterACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer) class Meta(BaseSerializer.Meta): model = CommandFilterACL fields = BaseSerializer.Meta.fields + ['command_groups'] + # 默认都支持所有的 actions + action_choices_exclude = [] class CommandReviewSerializer(serializers.Serializer): diff --git a/apps/acls/serializers/connect_method.py b/apps/acls/serializers/connect_method.py index b36fde06d..b1daf56ac 100644 --- a/apps/acls/serializers/connect_method.py +++ b/apps/acls/serializers/connect_method.py @@ -1,6 +1,7 @@ from orgs.mixins.serializers import BulkOrgResourceModelSerializer from .base import BaseUserAssetAccountACLSerializer as BaseSerializer from ..models import ConnectMethodACL +from ..const import ActionChoices __all__ = ["ConnectMethodACLSerializer"] @@ -12,12 +13,6 @@ class ConnectMethodACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer) i for i in BaseSerializer.Meta.fields + ['connect_methods'] if i not in ['assets', 'accounts'] ] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - field_action = self.fields.get('action') - if not field_action: - return - # 仅支持拒绝 - for k in ['review', 'accept']: - field_action._choices.pop(k, None) + action_choices_exclude = BaseSerializer.Meta.action_choices_exclude + [ + ActionChoices.review, ActionChoices.accept + ] diff --git a/apps/acls/serializers/login_acl.py b/apps/acls/serializers/login_acl.py index 1371bc091..c86424986 100644 --- a/apps/acls/serializers/login_acl.py +++ b/apps/acls/serializers/login_acl.py @@ -2,7 +2,7 @@ from django.utils.translation import ugettext as _ from common.serializers import MethodSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from .base import BaserUserACLSerializer +from .base import BaseUserACLSerializer from .rules import RuleSerializer from ..models import LoginACL @@ -11,12 +11,12 @@ __all__ = ["LoginACLSerializer"] common_help_text = _("With * indicating a match all. ") -class LoginACLSerializer(BaserUserACLSerializer, BulkOrgResourceModelSerializer): +class LoginACLSerializer(BaseUserACLSerializer, BulkOrgResourceModelSerializer): rules = MethodSerializer(label=_('Rule')) - class Meta(BaserUserACLSerializer.Meta): + class Meta(BaseUserACLSerializer.Meta): model = LoginACL - fields = BaserUserACLSerializer.Meta.fields + ['rules', ] + fields = BaseUserACLSerializer.Meta.fields + ['rules', ] def get_rules_serializer(self): return RuleSerializer() diff --git a/apps/assets/api/asset/__init__.py b/apps/assets/api/asset/__init__.py index 0f1d81825..75c314df7 100644 --- a/apps/assets/api/asset/__init__.py +++ b/apps/assets/api/asset/__init__.py @@ -3,6 +3,7 @@ from .cloud import * from .custom import * from .database import * from .device import * +from .gpt import * from .host import * from .permission import * from .web import * diff --git a/apps/assets/api/asset/asset.py b/apps/assets/api/asset/asset.py index 82733b03e..f1573629e 100644 --- a/apps/assets/api/asset/asset.py +++ b/apps/assets/api/asset/asset.py @@ -82,7 +82,7 @@ class AssetFilterSet(BaseFilterSet): @staticmethod def filter_protocols(queryset, name, value): value = value.split(',') - return queryset.filter(protocols__name__in=value) + return queryset.filter(protocols__name__in=value).distinct() @staticmethod def filter_labels(queryset, name, value): @@ -91,7 +91,7 @@ class AssetFilterSet(BaseFilterSet): queryset = queryset.filter(labels__name=n, labels__value=v) else: q = Q(labels__name__contains=value) | Q(labels__value__contains=value) - queryset = queryset.filter(q) + queryset = queryset.filter(q).distinct() return queryset @@ -121,6 +121,14 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet): NodeFilterBackend, AttrRulesFilterBackend ] + def get_queryset(self): + queryset = super().get_queryset() \ + .prefetch_related('nodes', 'protocols') \ + .select_related('platform', 'domain') + if queryset.model is not Asset: + queryset = queryset.select_related('asset_ptr') + return queryset + def get_serializer_class(self): cls = super().get_serializer_class() if self.action == "retrieve": diff --git a/apps/assets/api/asset/gpt.py b/apps/assets/api/asset/gpt.py new file mode 100644 index 000000000..ef9953a41 --- /dev/null +++ b/apps/assets/api/asset/gpt.py @@ -0,0 +1,16 @@ +from assets.models import GPT, Asset +from assets.serializers import GPTSerializer + +from .asset import AssetViewSet + +__all__ = ['GPTViewSet'] + + +class GPTViewSet(AssetViewSet): + model = GPT + perm_model = Asset + + def get_serializer_classes(self): + serializer_classes = super().get_serializer_classes() + serializer_classes['default'] = GPTSerializer + return serializer_classes diff --git a/apps/assets/api/category.py b/apps/assets/api/category.py index 8c14e352d..b6ec6790b 100644 --- a/apps/assets/api/category.py +++ b/apps/assets/api/category.py @@ -1,11 +1,11 @@ -from rest_framework.mixins import ListModelMixin from rest_framework.decorators import action +from rest_framework.mixins import ListModelMixin from rest_framework.response import Response +from assets.const import AllTypes +from assets.serializers import CategorySerializer, TypeSerializer from common.api import JMSGenericViewSet from common.permissions import IsValidUser -from assets.serializers import CategorySerializer, TypeSerializer -from assets.const import AllTypes __all__ = ['CategoryViewSet'] @@ -32,4 +32,3 @@ class CategoryViewSet(ListModelMixin, JMSGenericViewSet): tp = request.query_params.get('type') constraints = AllTypes.get_constraints(category, tp) return Response(constraints) - diff --git a/apps/assets/api/domain.py b/apps/assets/api/domain.py index f27f09c90..40d676d78 100644 --- a/apps/assets/api/domain.py +++ b/apps/assets/api/domain.py @@ -26,6 +26,8 @@ class DomainViewSet(OrgBulkModelViewSet): return serializers.DomainWithGatewaySerializer return serializers.DomainSerializer + def get_queryset(self): + return super().get_queryset().prefetch_related('assets') class GatewayViewSet(HostViewSet): perm_model = Gateway diff --git a/apps/assets/api/label.py b/apps/assets/api/label.py index cbb7b5bb3..d970d2180 100644 --- a/apps/assets/api/label.py +++ b/apps/assets/api/label.py @@ -38,5 +38,6 @@ class LabelViewSet(OrgBulkModelViewSet): return super().list(request, *args, **kwargs) def get_queryset(self): - self.queryset = Label.objects.annotate(asset_count=Count("assets")) + self.queryset = Label.objects.prefetch_related( + 'assets').annotate(asset_count=Count("assets")) return self.queryset diff --git a/apps/assets/api/platform.py b/apps/assets/api/platform.py index fc32a3b5f..a6e34b38b 100644 --- a/apps/assets/api/platform.py +++ b/apps/assets/api/platform.py @@ -4,20 +4,20 @@ from rest_framework.decorators import action from rest_framework.response import Response from assets.const import AllTypes -from assets.models import Platform, Node, Asset -from assets.serializers import PlatformSerializer +from assets.models import Platform, Node, Asset, PlatformProtocol +from assets.serializers import PlatformSerializer, PlatformProtocolSerializer from common.api import JMSModelViewSet from common.permissions import IsValidUser from common.serializers import GroupedChoiceSerializer -__all__ = ['AssetPlatformViewSet', 'PlatformAutomationMethodsApi'] +__all__ = ['AssetPlatformViewSet', 'PlatformAutomationMethodsApi', 'PlatformProtocolViewSet'] class AssetPlatformViewSet(JMSModelViewSet): queryset = Platform.objects.all() serializer_classes = { 'default': PlatformSerializer, - 'categories': GroupedChoiceSerializer + 'categories': GroupedChoiceSerializer, } filterset_fields = ['name', 'category', 'type'] search_fields = ['name'] @@ -25,7 +25,7 @@ class AssetPlatformViewSet(JMSModelViewSet): 'categories': 'assets.view_platform', 'type_constraints': 'assets.view_platform', 'ops_methods': 'assets.view_platform', - 'filter_nodes_assets': 'assets.view_platform' + 'filter_nodes_assets': 'assets.view_platform', } def get_queryset(self): @@ -61,6 +61,15 @@ class AssetPlatformViewSet(JMSModelViewSet): return Response(serializer.data) +class PlatformProtocolViewSet(JMSModelViewSet): + queryset = PlatformProtocol.objects.all() + serializer_class = PlatformProtocolSerializer + filterset_fields = ['name', 'platform__name'] + rbac_perms = { + '*': 'assets.add_platform' + } + + class PlatformAutomationMethodsApi(generics.ListAPIView): permission_classes = (IsValidUser,) diff --git a/apps/assets/api/tree.py b/apps/assets/api/tree.py index 794e093b7..1970d33ba 100644 --- a/apps/assets/api/tree.py +++ b/apps/assets/api/tree.py @@ -127,10 +127,13 @@ class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi): if not self.instance or not include_assets: return Asset.objects.none() if query_all: - assets = self.instance.get_all_assets_for_tree() + assets = self.instance.get_all_assets() else: - assets = self.instance.get_assets_for_tree() - return assets + assets = self.instance.get_assets() + return assets.only( + "id", "name", "address", "platform_id", + "org_id", "is_active", 'comment' + ).prefetch_related('platform') def filter_queryset_for_assets(self, assets): search = self.request.query_params.get('search') diff --git a/apps/assets/automations/ping/custom/rdp/main.yml b/apps/assets/automations/ping/custom/rdp/main.yml new file mode 100644 index 000000000..75e40c027 --- /dev/null +++ b/apps/assets/automations/ping/custom/rdp/main.yml @@ -0,0 +1,15 @@ +- hosts: custom + gather_facts: no + vars: + ansible_shell_type: sh + ansible_connection: local + + tasks: + - name: Test asset connection (pyfreerdp) + rdp_ping: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_secret_type: "{{ jms_account.secret_type }}" + login_private_key_path: "{{ jms_account.private_key_path }}" diff --git a/apps/assets/automations/ping/custom/rdp/manifest.yml b/apps/assets/automations/ping/custom/rdp/manifest.yml new file mode 100644 index 000000000..77b8a855e --- /dev/null +++ b/apps/assets/automations/ping/custom/rdp/manifest.yml @@ -0,0 +1,13 @@ +id: ping_by_rdp +name: "{{ 'Ping by pyfreerdp' | trans }}" +category: + - device + - host +type: + - windows +method: ping +i18n: + Ping by pyfreerdp: + zh: 使用 Python 模块 pyfreerdp 测试主机可连接性 + en: Ping by pyfreerdp module + ja: Pyfreerdpモジュールを使用してホストにPingする diff --git a/apps/assets/automations/ping/custom/main.yml b/apps/assets/automations/ping/custom/ssh/main.yml similarity index 90% rename from apps/assets/automations/ping/custom/main.yml rename to apps/assets/automations/ping/custom/ssh/main.yml index 50911847b..9725f63d7 100644 --- a/apps/assets/automations/ping/custom/main.yml +++ b/apps/assets/automations/ping/custom/ssh/main.yml @@ -4,7 +4,7 @@ ansible_connection: local tasks: - - name: Test asset connection + - name: Test asset connection (paramiko) ssh_ping: login_user: "{{ jms_account.username }}" login_password: "{{ jms_account.secret }}" diff --git a/apps/assets/automations/ping/custom/manifest.yml b/apps/assets/automations/ping/custom/ssh/manifest.yml similarity index 100% rename from apps/assets/automations/ping/custom/manifest.yml rename to apps/assets/automations/ping/custom/ssh/manifest.yml diff --git a/apps/assets/const/base.py b/apps/assets/const/base.py index 99ff06314..66c991caa 100644 --- a/apps/assets/const/base.py +++ b/apps/assets/const/base.py @@ -1,7 +1,8 @@ +from django.db import models from django.db.models import TextChoices +from django.utils.translation import gettext_lazy as _ from jumpserver.utils import has_valid_xpack_license -from .protocol import Protocol class Type: @@ -28,6 +29,12 @@ class Type: ) +class FillType(models.TextChoices): + no = 'no', _('Disabled') + basic = 'basic', _('Basic') + script = 'script', _('Script') + + class BaseType(TextChoices): """ 约束应该考虑代是对平台对限制,避免多余对选项,如: mysql 开启 ssh, @@ -49,7 +56,7 @@ class BaseType(TextChoices): for k, v in cls.get_choices(): tp_base = {**base_default, **base.get(k, {})} tp_auto = {**automation_default, **automation.get(k, {})} - tp_protocols = {**protocols_default, **protocols.get(k, {})} + tp_protocols = {**protocols_default, **{'port_from_addr': False}, **protocols.get(k, {})} tp_protocols = cls._parse_protocols(tp_protocols, k) tp_constrains = {**tp_base, 'protocols': tp_protocols, 'automation': tp_auto} constrains[k] = tp_constrains @@ -57,14 +64,20 @@ class BaseType(TextChoices): @classmethod def _parse_protocols(cls, protocol, tp): + from .protocol import Protocol settings = Protocol.settings() choices = protocol.get('choices', []) if choices == '__self__': choices = [tp] - protocols = [ - {'name': name, **settings.get(name, {})} - for name in choices - ] + + protocols = [] + for name in choices: + protocol = {'name': name, **settings.get(name, {})} + setting = protocol.pop('setting', {}) + setting_values = {k: v.get('default', None) for k, v in setting.items()} + protocol['setting'] = setting_values + protocols.append(protocol) + if protocols: protocols[0]['default'] = True return protocols diff --git a/apps/assets/const/category.py b/apps/assets/const/category.py index 8c4d387d8..9ccbb134e 100644 --- a/apps/assets/const/category.py +++ b/apps/assets/const/category.py @@ -12,6 +12,7 @@ class Category(ChoicesMixin, models.TextChoices): DATABASE = 'database', _("Database") CLOUD = 'cloud', _("Cloud service") WEB = 'web', _("Web") + GPT = 'gpt', "GPT" CUSTOM = 'custom', _("Custom type") @classmethod diff --git a/apps/assets/const/custom.py b/apps/assets/const/custom.py index 5c72127b3..878207f26 100644 --- a/apps/assets/const/custom.py +++ b/apps/assets/const/custom.py @@ -1,3 +1,6 @@ +from collections import defaultdict + +from common.decorators import cached_method from .base import BaseType @@ -9,7 +12,8 @@ class CustomTypes(BaseType): except Exception: return [] types = set([p.type for p in platforms]) - return [(t, t) for t in types] + choices = [(t, t) for t in types] + return choices @classmethod def _get_base_constrains(cls) -> dict: @@ -37,13 +41,20 @@ class CustomTypes(BaseType): return constrains @classmethod + @cached_method(5) def _get_protocol_constrains(cls) -> dict: - constrains = {} - for platform in cls.get_custom_platforms(): - choices = list(platform.protocols.values_list('name', flat=True)) - if platform.type in constrains: - choices = constrains[platform.type]['choices'] + choices - constrains[platform.type] = {'choices': choices} + from assets.models import PlatformProtocol + _constrains = defaultdict(set) + protocols = PlatformProtocol.objects \ + .filter(platform__category='custom') \ + .values_list('name', 'platform__type') + for name, tp in protocols: + _constrains[tp].add(name) + + constrains = { + tp: {'choices': list(choices)} + for tp, choices in _constrains.items() + } return constrains @classmethod @@ -51,6 +62,8 @@ class CustomTypes(BaseType): return {} @classmethod + @cached_method(5) def get_custom_platforms(cls): from assets.models import Platform - return Platform.objects.filter(category='custom') + platforms = Platform.objects.filter(category='custom') + return platforms diff --git a/apps/assets/const/gpt.py b/apps/assets/const/gpt.py new file mode 100644 index 000000000..65d01ee97 --- /dev/null +++ b/apps/assets/const/gpt.py @@ -0,0 +1,54 @@ +from django.utils.translation import gettext_lazy as _ + +from .base import BaseType + + +class GPTTypes(BaseType): + CHATGPT = 'chatgpt', _('ChatGPT') + + @classmethod + def _get_base_constrains(cls) -> dict: + return { + '*': { + 'charset_enabled': False, + 'domain_enabled': False, + 'su_enabled': False, + } + } + + @classmethod + def _get_automation_constrains(cls) -> dict: + constrains = { + '*': { + 'ansible_enabled': False, + 'ping_enabled': False, + 'gather_facts_enabled': False, + 'verify_account_enabled': False, + 'change_secret_enabled': False, + 'push_account_enabled': False, + 'gather_accounts_enabled': False, + } + } + return constrains + + @classmethod + def _get_protocol_constrains(cls) -> dict: + return { + '*': { + 'choices': '__self__', + } + } + + @classmethod + def internal_platforms(cls): + return { + cls.CHATGPT: [ + {'name': 'ChatGPT'} + ], + } + + @classmethod + def get_community_types(cls): + return [ + cls.CHATGPT, + ] diff --git a/apps/assets/const/protocol.py b/apps/assets/const/protocol.py index a0b7b7ec7..aface581c 100644 --- a/apps/assets/const/protocol.py +++ b/apps/assets/const/protocol.py @@ -1,6 +1,10 @@ +from django.conf import settings from django.db import models +from django.utils.translation import gettext_lazy as _ from common.db.models import ChoicesMixin +from common.decorators import cached_method +from .base import FillType __all__ = ['Protocol'] @@ -22,8 +26,9 @@ class Protocol(ChoicesMixin, models.TextChoices): mongodb = 'mongodb', 'MongoDB' k8s = 'k8s', 'K8S' - http = 'http', 'HTTP' - _settings = None + http = 'http', 'HTTP(s)' + + chatgpt = 'chatgpt', 'ChatGPT' @classmethod def device_protocols(cls): @@ -32,16 +37,41 @@ class Protocol(ChoicesMixin, models.TextChoices): 'port': 22, 'secret_types': ['password', 'ssh_key'], 'setting': { - 'sftp_enabled': True, - 'sftp_home': '/tmp', + 'sftp_enabled': { + 'type': 'bool', + 'default': True, + 'label': _('SFTP enabled') + }, + 'sftp_home': { + 'type': 'str', + 'default': '/tmp', + 'label': _('SFTP home') + }, } }, cls.rdp: { 'port': 3389, 'secret_types': ['password'], 'setting': { - 'console': False, - 'security': 'any', + 'console': { + 'type': 'bool', + 'default': False, + 'label': _('Console'), + 'help_text': _("Connect to console session") + }, + 'security': { + 'type': 'choice', + 'choices': [('any', _('Any')), ('rdp', 'RDP'), ('tls', 'TLS'), ('nla', 'NLA')], + 'default': 'any', + 'label': _('Security'), + 'help_text': _("Security layer to use for the connection") + }, + 'ad_domain': { + 'type': 'str', + 'required': False, + 'default': '', + 'label': _('AD domain') + } } }, cls.vnc: { @@ -56,7 +86,11 @@ class Protocol(ChoicesMixin, models.TextChoices): 'port': 5985, 'secret_types': ['password'], 'setting': { - 'use_ssl': False, + 'use_ssl': { + 'type': 'bool', + 'default': False, + 'label': _('Use SSL') + }, } }, } @@ -79,21 +113,25 @@ class Protocol(ChoicesMixin, models.TextChoices): 'port': 5432, 'required': True, 'secret_types': ['password'], + 'xpack': True }, cls.oracle: { 'port': 1521, 'required': True, 'secret_types': ['password'], + 'xpack': True }, cls.sqlserver: { 'port': 1433, 'required': True, 'secret_types': ['password'], + 'xpack': True, }, cls.clickhouse: { 'port': 9000, 'required': True, 'secret_types': ['password'], + 'xpack': True, }, cls.mongodb: { 'port': 27017, @@ -105,7 +143,11 @@ class Protocol(ChoicesMixin, models.TextChoices): 'required': True, 'secret_types': ['password'], 'setting': { - 'auth_username': True, + 'auth_username': { + 'type': 'bool', + 'default': False, + 'label': _('Auth username') + }, } }, } @@ -115,32 +157,97 @@ class Protocol(ChoicesMixin, models.TextChoices): return { cls.k8s: { 'port': 443, + 'port_from_addr': True, 'required': True, 'secret_types': ['token'], }, cls.http: { 'port': 80, + 'port_from_addr': True, 'secret_types': ['password'], 'setting': { - 'username_selector': 'name=username', - 'password_selector': 'name=password', - 'submit_selector': 'id=login_button', + 'autofill': { + 'label': _('Autofill'), + 'type': 'choice', + 'choices': FillType.choices, + 'default': 'basic', + }, + 'username_selector': { + 'type': 'str', + 'default': 'name=username', + 'label': _('Username selector') + }, + 'password_selector': { + 'type': 'str', + 'default': 'name=password', + 'label': _('Password selector') + }, + 'submit_selector': { + 'type': 'str', + 'default': 'type=submit', + 'label': _('Submit selector') + }, + 'script': { + 'type': 'text', + 'default': [], + 'label': _('Script'), + } } }, } @classmethod + def gpt_protocols(cls): + protocols = { + cls.chatgpt: { + 'port': 443, + 'required': True, + 'port_from_addr': True, + 'secret_types': ['api_key'], + 'setting': { + 'api_mode': { + 'type': 'choice', + 'default': 'gpt-3.5-turbo', + 'label': _('API mode'), + 'choices': [ + ('gpt-3.5-turbo', 'GPT-3.5 Turbo'), + ('gpt-3.5-turbo-16k', 'GPT-3.5 Turbo 16K'), + ] + } + } + } + } + if settings.XPACK_ENABLED: + choices = protocols[cls.chatgpt]['setting']['api_mode']['choices'] + choices.extend([ + ('gpt-4', 'GPT-4'), + ('gpt-4-32k', 'GPT-4 32K'), + ]) + return protocols + + @classmethod + @cached_method(ttl=600) def settings(cls): return { **cls.device_protocols(), **cls.database_protocols(), - **cls.cloud_protocols() + **cls.cloud_protocols(), + **cls.gpt_protocols(), } + @classmethod + @cached_method(ttl=600) + def xpack_protocols(cls): + return [ + protocol + for protocol, config in cls.settings().items() + if config.get('xpack', False) + ] + @classmethod def protocol_secret_types(cls): - settings = cls.settings() + configs = cls.settings() return { - protocol: settings[protocol]['secret_types'] or ['password'] - for protocol in cls.settings() + protocol: configs[protocol]['secret_types'] or ['password'] + for protocol in configs } diff --git a/apps/assets/const/types.py b/apps/assets/const/types.py index 69fa36bb3..afa0f6d05 100644 --- a/apps/assets/const/types.py +++ b/apps/assets/const/types.py @@ -10,6 +10,7 @@ from .cloud import CloudTypes from .custom import CustomTypes from .database import DatabaseTypes from .device import DeviceTypes +from .gpt import GPTTypes from .host import HostTypes from .web import WebTypes @@ -18,7 +19,7 @@ class AllTypes(ChoicesMixin): choices: list includes = [ HostTypes, DeviceTypes, DatabaseTypes, - CloudTypes, WebTypes, CustomTypes + CloudTypes, WebTypes, CustomTypes, GPTTypes ] _category_constrains = {} @@ -147,6 +148,7 @@ class AllTypes(ChoicesMixin): (Category.DATABASE, DatabaseTypes), (Category.CLOUD, CloudTypes), (Category.WEB, WebTypes), + (Category.GPT, GPTTypes), (Category.CUSTOM, CustomTypes), ) @@ -193,7 +195,6 @@ class AllTypes(ChoicesMixin): } return node - @classmethod def asset_to_node(cls, asset, pid): node = { @@ -351,7 +352,7 @@ class AllTypes(ChoicesMixin): for d in platform_datas: name = d['name'] - # print("\t - Platform: {}".format(name)) + print("\t - Platform: {}".format(name)) _automation = d.pop('automation', {}) _protocols = d.pop('_protocols', []) _protocols_setting = d.pop('protocols_setting', {}) @@ -364,7 +365,7 @@ class AllTypes(ChoicesMixin): setting = _protocols_setting.get(p['name'], {}) p['required'] = setting.pop('required', False) p['default'] = setting.pop('default', False) - p['setting'] = {**p.get('setting', {}), **setting} + p['setting'] = {**p.get('setting', {}).get('default', ''), **setting} platform_data = { **default_platform_data, **d, diff --git a/apps/assets/const/web.py b/apps/assets/const/web.py index bf9d9e3c8..42ea995ac 100644 --- a/apps/assets/const/web.py +++ b/apps/assets/const/web.py @@ -1,4 +1,3 @@ -from django.db import models from django.utils.translation import gettext_lazy as _ from .base import BaseType @@ -53,9 +52,3 @@ class WebTypes(BaseType): return [ cls.WEBSITE, ] - - -class FillType(models.TextChoices): - no = 'no', _('Disabled') - basic = 'basic', _('Basic') - script = 'script', _('Script') diff --git a/apps/assets/migrations/0093_auto_20220403_1627.py b/apps/assets/migrations/0093_auto_20220403_1627.py index c34c70ed7..484da9740 100644 --- a/apps/assets/migrations/0093_auto_20220403_1627.py +++ b/apps/assets/migrations/0093_auto_20220403_1627.py @@ -2,6 +2,7 @@ import django.db from django.db import migrations, models + import common.db.fields @@ -118,7 +119,7 @@ class Migration(migrations.Migration): primary_key=True, serialize=False, to='assets.asset')), ], options={ - 'verbose_name': 'Host', + 'verbose_name': 'Host', }, ), migrations.CreateModel( diff --git a/apps/assets/migrations/0098_auto_20220430_2126.py b/apps/assets/migrations/0098_auto_20220430_2126.py index f388ca731..8d1cfdc5e 100644 --- a/apps/assets/migrations/0098_auto_20220430_2126.py +++ b/apps/assets/migrations/0098_auto_20220430_2126.py @@ -137,6 +137,25 @@ def migrate_to_nodes(apps, *args): parent.save() +def migrate_ori_host_to_devices(apps, *args): + device_model = apps.get_model('assets', 'Device') + asset_model = apps.get_model('assets', 'Asset') + host_model = apps.get_model('assets', 'Host') + hosts_need_migrate_to_device = host_model.objects.filter(asset_ptr__platform__category='device') + assets = asset_model.objects.filter(id__in=hosts_need_migrate_to_device.values_list('asset_ptr_id', flat=True)) + assets_map = {asset.id: asset for asset in assets} + + print("\t- Migrate ori host to device: ", len(hosts_need_migrate_to_device)) + for host in hosts_need_migrate_to_device: + asset = assets_map.get(host.asset_ptr_id) + if not asset: + continue + device = device_model(asset_ptr_id=asset.id) + device.__dict__.update(asset.__dict__) + device.save() + host.delete(keep_parents=True) + + class Migration(migrations.Migration): dependencies = [ ('assets', '0097_auto_20220426_1558'), @@ -146,5 +165,6 @@ class Migration(migrations.Migration): operations = [ migrations.RunPython(migrate_database_to_asset), migrations.RunPython(migrate_cloud_to_asset), - migrations.RunPython(migrate_to_nodes) + migrations.RunPython(migrate_to_nodes), + migrations.RunPython(migrate_ori_host_to_devices), ] diff --git a/apps/assets/migrations/0114_baseautomation_params.py b/apps/assets/migrations/0114_baseautomation_params.py index 857c5d795..b48f5b302 100644 --- a/apps/assets/migrations/0114_baseautomation_params.py +++ b/apps/assets/migrations/0114_baseautomation_params.py @@ -2,16 +2,13 @@ from django.db import migrations, models -from assets.const import AllTypes - def migrate_automation_push_account_params(apps, schema_editor): platform_automation_model = apps.get_model('assets', 'PlatformAutomation') - platform_automation_methods = AllTypes.get_automation_methods() methods_id_data_map = { - i['id']: None if i['params_serializer'] is None else i['params_serializer']({}).data - for i in platform_automation_methods - if i['method'] == 'push_account' + 'push_account_aix': {'sudo': '/bin/whoami', 'shell': '/bin/bash', 'home': '', 'groups': ''}, + 'push_account_posix': {'sudo': '/bin/whoami', 'shell': '/bin/bash', 'home': '', 'groups': ''}, + 'push_account_local_windows': {'groups': 'Users,Remote Desktop Users'}, } automation_objs = [] for automation in platform_automation_model.objects.all(): diff --git a/apps/assets/migrations/0120_auto_20230630_1613.py b/apps/assets/migrations/0120_auto_20230630_1613.py new file mode 100644 index 000000000..a5205804b --- /dev/null +++ b/apps/assets/migrations/0120_auto_20230630_1613.py @@ -0,0 +1,39 @@ +# Generated by Django 3.2.19 on 2023-06-30 08:13 + +import django.db.models.deletion +from django.db import migrations, models + + +def add_chatgpt_platform(apps, schema_editor): + platform_cls = apps.get_model('assets', 'Platform') + automation_cls = apps.get_model('assets', 'PlatformAutomation') + platform = platform_cls.objects.create( + name='ChatGPT', internal=True, category='gpt', type='chatgpt', + domain_enabled=False, su_enabled=False, comment='ChatGPT', + created_by='System', updated_by='System', + ) + platform.protocols.create(name='chatgpt', port=443, primary=True, setting={'api_mode': 'gpt-3.5-turbo'}) + automation_cls.objects.create(ansible_enabled=False, platform=platform) + + +class Migration(migrations.Migration): + dependencies = [ + ('assets', '0119_assets_add_default_node'), + ] + + operations = [ + migrations.CreateModel( + name='GPT', + fields=[ + ('asset_ptr', + models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, + primary_key=True, serialize=False, to='assets.asset')), + ('proxy', models.CharField(blank=True, default='', max_length=128, verbose_name='Proxy')), + ], + options={ + 'verbose_name': 'Web', + }, + bases=('assets.asset',), + ), + migrations.RunPython(add_chatgpt_platform) + ] diff --git a/apps/assets/models/asset/__init__.py b/apps/assets/models/asset/__init__.py index 0004bfbb5..7541f2f2e 100644 --- a/apps/assets/models/asset/__init__.py +++ b/apps/assets/models/asset/__init__.py @@ -3,5 +3,6 @@ from .common import * from .custom import * from .database import * from .device import * +from .gpt import * from .host import * from .web import * diff --git a/apps/assets/models/asset/common.py b/apps/assets/models/asset/common.py index 6e6d6b836..7a564f787 100644 --- a/apps/assets/models/asset/common.py +++ b/apps/assets/models/asset/common.py @@ -206,15 +206,14 @@ class Asset(NodesRelationMixin, AbsConnectivity, JSONFilterMixin, JMSOrgBaseMode @lazyproperty def auto_config(self): platform = self.platform - automation = self.platform.automation auto_config = { 'su_enabled': platform.su_enabled, 'domain_enabled': platform.domain_enabled, 'ansible_enabled': False } + automation = getattr(self.platform, 'automation', None) if not automation: return auto_config - auto_config.update(model_to_dict(automation)) return auto_config diff --git a/apps/assets/models/asset/gpt.py b/apps/assets/models/asset/gpt.py new file mode 100644 index 000000000..4522dfe93 --- /dev/null +++ b/apps/assets/models/asset/gpt.py @@ -0,0 +1,11 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from .common import Asset + + +class GPT(Asset): + proxy = models.CharField(max_length=128, blank=True, default='', verbose_name=_("Proxy")) + + class Meta: + verbose_name = _("Web") diff --git a/apps/assets/models/asset/web.py b/apps/assets/models/asset/web.py index a12965334..d46ce00c4 100644 --- a/apps/assets/models/asset/web.py +++ b/apps/assets/models/asset/web.py @@ -1,7 +1,7 @@ from django.db import models from django.utils.translation import gettext_lazy as _ -from assets.const.web import FillType +from assets.const import FillType from .common import Asset diff --git a/apps/assets/models/group.py b/apps/assets/models/group.py index ee5472622..02db7f41f 100644 --- a/apps/assets/models/group.py +++ b/apps/assets/models/group.py @@ -7,12 +7,9 @@ from __future__ import unicode_literals import uuid from django.db import models -import logging from django.utils.translation import ugettext_lazy as _ - __all__ = ['AssetGroup'] -logger = logging.getLogger(__name__) class AssetGroup(models.Model): diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index 3a729ba9f..94ff9aba5 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -429,18 +429,6 @@ class NodeAssetsMixin(NodeAllAssetsMappingMixin): assets = Asset.objects.filter(nodes=self) return assets.distinct() - def get_assets_for_tree(self): - return self.get_assets().only( - "id", "name", "address", "platform_id", - "org_id", "is_active" - ).prefetch_related('platform') - - def get_all_assets_for_tree(self): - return self.get_all_assets().only( - "id", "name", "address", "platform_id", - "org_id", "is_active" - ).prefetch_related('platform') - def get_valid_assets(self): return self.get_assets().valid() diff --git a/apps/assets/models/platform.py b/apps/assets/models/platform.py index f50177642..8fed01acf 100644 --- a/apps/assets/models/platform.py +++ b/apps/assets/models/platform.py @@ -8,6 +8,8 @@ from common.db.models import JMSBaseModel __all__ = ['Platform', 'PlatformProtocol', 'PlatformAutomation'] +from common.utils import lazyproperty + class PlatformProtocol(models.Model): name = models.CharField(max_length=32, verbose_name=_('Name')) @@ -26,6 +28,11 @@ class PlatformProtocol(models.Model): def secret_types(self): return Protocol.settings().get(self.name, {}).get('secret_types', ['password']) + @lazyproperty + def port_from_addr(self): + from assets.const.protocol import Protocol as ProtocolConst + return ProtocolConst.settings().get(self.name, {}).get('port_from_addr', False) + class PlatformAutomation(models.Model): ansible_enabled = models.BooleanField(default=False, verbose_name=_("Enabled")) diff --git a/apps/assets/serializers/asset/__init__.py b/apps/assets/serializers/asset/__init__.py index 8e3e14cf3..481e90863 100644 --- a/apps/assets/serializers/asset/__init__.py +++ b/apps/assets/serializers/asset/__init__.py @@ -4,5 +4,6 @@ from .common import * from .custom import * from .database import * from .device import * +from .gpt import * from .host import * from .web import * diff --git a/apps/assets/serializers/asset/common.py b/apps/assets/serializers/asset/common.py index ba642dd96..69ccf8e21 100644 --- a/apps/assets/serializers/asset/common.py +++ b/apps/assets/serializers/asset/common.py @@ -124,6 +124,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=()) accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, write_only=True, label=_('Account')) nodes_display = serializers.ListField(read_only=False, required=False, label=_("Node path")) + _accounts = None class Meta: model = Asset @@ -151,6 +152,13 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._init_field_choices() + self._extract_accounts() + + def _extract_accounts(self): + if not getattr(self, 'initial_data', None): + return + accounts = self.initial_data.pop('accounts', None) + self._accounts = accounts def _get_protocols_required_default(self): platform = self._asset_platform @@ -167,10 +175,9 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali return protocols_required, protocols_default = self._get_protocols_required_default() - protocols_data = [ - {'name': p.name, 'port': p.port} - for p in protocols_required + protocols_default - ] + protocol_map = {str(protocol.id): protocol for protocol in protocols_required + protocols_default} + protocols = list(protocol_map.values()) + protocols_data = [{'name': p.name, 'port': p.port} for p in protocols] self.initial_data['protocols'] = protocols_data def _init_field_choices(self): @@ -263,7 +270,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali error = p.get('name') + ': ' + _("port out of range (0-65535)") raise serializers.ValidationError(error) - protocols_required, protocols_default = self._get_protocols_required_default() + protocols_required, __ = self._get_protocols_required_default() protocols_not_found = [p.name for p in protocols_required if p.name not in protocols_data_map] if protocols_not_found: raise serializers.ValidationError({ @@ -277,7 +284,6 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali return for data in accounts_data: data['asset'] = asset.id - s = AssetAccountSerializer(data=accounts_data, many=True) s.is_valid(raise_exception=True) s.save() @@ -285,16 +291,13 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali @atomic def create(self, validated_data): nodes_display = validated_data.pop('nodes_display', '') - accounts = validated_data.pop('accounts', []) instance = super().create(validated_data) - self.accounts_create(accounts, instance) + self.accounts_create(self._accounts, instance) self.perform_nodes_display_create(instance, nodes_display) return instance @atomic def update(self, instance, validated_data): - if not validated_data.get('accounts'): - validated_data.pop('accounts', None) nodes_display = validated_data.pop('nodes_display', '') instance = super().update(instance, validated_data) self.perform_nodes_display_create(instance, nodes_display) diff --git a/apps/assets/serializers/asset/gpt.py b/apps/assets/serializers/asset/gpt.py new file mode 100644 index 000000000..4bcc16877 --- /dev/null +++ b/apps/assets/serializers/asset/gpt.py @@ -0,0 +1,33 @@ +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from assets.models import GPT +from .common import AssetSerializer + +__all__ = ['GPTSerializer'] + + +class GPTSerializer(AssetSerializer): + class Meta(AssetSerializer.Meta): + model = GPT + fields = AssetSerializer.Meta.fields + [ + 'proxy', + ] + extra_kwargs = { + **AssetSerializer.Meta.extra_kwargs, + 'proxy': { + 'help_text': _( + 'If the server cannot directly connect to the API address, ' + 'you need set up an HTTP proxy. ' + 'e.g. http(s)://host:port' + ), + 'label': _('HTTP proxy')} + } + + @staticmethod + def validate_proxy(value): + if value and not value.startswith(("http://", "https://")): + raise serializers.ValidationError( + _('Proxy must start with http:// or https://') + ) + return value diff --git a/apps/assets/serializers/asset/info/spec.py b/apps/assets/serializers/asset/info/spec.py index 72be7edec..9a3dab960 100644 --- a/apps/assets/serializers/asset/info/spec.py +++ b/apps/assets/serializers/asset/info/spec.py @@ -1,7 +1,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from assets.const.web import FillType +from assets.const import FillType from assets.models import Database, Web from common.serializers.fields import LabeledChoiceField @@ -14,6 +14,7 @@ class DatabaseSpecSerializer(serializers.ModelSerializer): class WebSpecSerializer(serializers.ModelSerializer): autofill = LabeledChoiceField(choices=FillType.choices, label=_('Autofill')) + class Meta: model = Web fields = [ diff --git a/apps/assets/serializers/automations/base.py b/apps/assets/serializers/automations/base.py index 527f71628..d930fd1dc 100644 --- a/apps/assets/serializers/automations/base.py +++ b/apps/assets/serializers/automations/base.py @@ -51,14 +51,14 @@ class AutomationExecutionSerializer(serializers.ModelSerializer): from assets.const import AutomationTypes as AssetTypes from accounts.const import AutomationTypes as AccountTypes tp_dict = dict(AssetTypes.choices) | dict(AccountTypes.choices) - tp = obj.snapshot['type'] + tp = obj.snapshot.get('type', '') snapshot = { 'type': {'value': tp, 'label': tp_dict.get(tp, tp)}, - 'name': obj.snapshot['name'], - 'comment': obj.snapshot['comment'], - 'accounts': obj.snapshot['accounts'], - 'node_amount': len(obj.snapshot['nodes']), - 'asset_amount': len(obj.snapshot['assets']), + 'name': obj.snapshot.get('name'), + 'comment': obj.snapshot.get('comment'), + 'accounts': obj.snapshot.get('accounts'), + 'node_amount': len(obj.snapshot.get('nodes', [])), + 'asset_amount': len(obj.snapshot.get('assets', [])), } return snapshot diff --git a/apps/assets/serializers/platform.py b/apps/assets/serializers/platform.py index b78e134f4..d42eaab19 100644 --- a/apps/assets/serializers/platform.py +++ b/apps/assets/serializers/platform.py @@ -1,48 +1,17 @@ +from django.db.models import QuerySet from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from assets.const.web import FillType -from common.serializers import WritableNestedModelSerializer, type_field_map +from common.serializers import ( + WritableNestedModelSerializer, type_field_map, MethodSerializer, + DictSerializer, create_serializer_class +) from common.serializers.fields import LabeledChoiceField from common.utils import lazyproperty -from ..const import Category, AllTypes +from ..const import Category, AllTypes, Protocol from ..models import Platform, PlatformProtocol, PlatformAutomation -__all__ = ["PlatformSerializer", "PlatformOpsMethodSerializer"] - - -class ProtocolSettingSerializer(serializers.Serializer): - SECURITY_CHOICES = [ - ("any", "Any"), - ("rdp", "RDP"), - ("tls", "TLS"), - ("nla", "NLA"), - ] - # RDP - console = serializers.BooleanField(required=False, default=False) - security = serializers.ChoiceField(choices=SECURITY_CHOICES, default="any") - - # SFTP - sftp_enabled = serializers.BooleanField(default=True, label=_("SFTP enabled")) - sftp_home = serializers.CharField(default="/tmp", label=_("SFTP home")) - - # HTTP - autofill = serializers.ChoiceField(default='basic', choices=FillType.choices, label=_("Autofill")) - username_selector = serializers.CharField( - default="", allow_blank=True, label=_("Username selector") - ) - password_selector = serializers.CharField( - default="", allow_blank=True, label=_("Password selector") - ) - submit_selector = serializers.CharField( - default="", allow_blank=True, label=_("Submit selector") - ) - script = serializers.JSONField(default=list, label=_("Script")) - # Redis - auth_username = serializers.BooleanField(default=False, label=_("Auth with username")) - - # WinRM - use_ssl = serializers.BooleanField(default=False, label=_("Use SSL")) +__all__ = ["PlatformSerializer", "PlatformOpsMethodSerializer", "PlatformProtocolSerializer"] class PlatformAutomationSerializer(serializers.ModelSerializer): @@ -76,15 +45,57 @@ class PlatformAutomationSerializer(serializers.ModelSerializer): class PlatformProtocolSerializer(serializers.ModelSerializer): - setting = ProtocolSettingSerializer(required=False, allow_null=True) + setting = MethodSerializer(required=False, label=_("Setting")) + port_from_addr = serializers.BooleanField(label=_("Port from addr"), read_only=True) class Meta: model = PlatformProtocol fields = [ - "id", "name", "port", "primary", - "required", "default", "public", + "id", "name", "port", "port_from_addr", + "primary", "required", "default", "public", "secret_types", "setting", ] + extra_kwargs = { + "primary": { + "help_text": _( + "This protocol is primary, and it must be set when adding assets. " + "Additionally, there can only be one primary protocol." + ) + }, + "required": { + "help_text": _("This protocol is required, and it must be set when adding assets.") + }, + "default": { + "help_text": _("This protocol is default, when adding assets, it will be displayed by default.") + }, + "public": { + "help_text": _("This protocol is public, asset will show this protocol to user") + }, + } + + def get_setting_serializer(self): + request = self.context.get('request') + default_field = DictSerializer(required=False) + + if not request: + return default_field + + if self.instance and isinstance(self.instance, (QuerySet, list)): + instance = self.instance[0] + else: + instance = self.instance + + protocol = request.query_params.get('name', '') + if instance and not protocol: + protocol = instance.name + + protocol_settings = Protocol.settings() + setting_fields = protocol_settings.get(protocol, {}).get('setting') + if not setting_fields: + return default_field + setting_fields = [{'name': k, **v} for k, v in setting_fields.items()] + name = '{}ProtocolSettingSerializer'.format(protocol.capitalize()) + return create_serializer_class(name, setting_fields)() def to_file_representation(self, data): return '{name}/{port}'.format(**data) @@ -144,6 +155,18 @@ class PlatformSerializer(WritableNestedModelSerializer): "domain_default": {"label": _('Default Domain')}, } + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.set_initial_value() + + def set_initial_value(self): + if not hasattr(self, 'initial_data'): + return + if self.instance: + return + if not self.initial_data.get('automation'): + self.initial_data['automation'] = {} + @property def platform_category_type(self): if self.instance: @@ -189,8 +212,9 @@ class PlatformSerializer(WritableNestedModelSerializer): def validate_automation(self, automation): automation = automation or {} - automation = automation.get('ansible_enabled', False) \ - and self.constraints['automation'].get('ansible_enabled', False) + ansible_enabled = automation.get('ansible_enabled', False) \ + and self.constraints['automation'].get('ansible_enabled', False) + automation['ansible_enable'] = ansible_enabled return automation diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index 6b5f469d0..983e077f0 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -14,6 +14,7 @@ router.register(r'devices', api.DeviceViewSet, 'device') router.register(r'databases', api.DatabaseViewSet, 'database') router.register(r'webs', api.WebViewSet, 'web') router.register(r'clouds', api.CloudViewSet, 'cloud') +router.register(r'gpts', api.GPTViewSet, 'gpt') router.register(r'customs', api.CustomViewSet, 'custom') router.register(r'platforms', api.AssetPlatformViewSet, 'platform') router.register(r'labels', api.LabelViewSet, 'label') @@ -21,6 +22,7 @@ router.register(r'nodes', api.NodeViewSet, 'node') router.register(r'domains', api.DomainViewSet, 'domain') router.register(r'gateways', api.GatewayViewSet, 'gateway') router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset') +router.register(r'protocol-settings', api.PlatformProtocolViewSet, 'protocol-setting') urlpatterns = [ # path('assets//gateways/', api.AssetGatewayListApi.as_view(), name='asset-gateway-list'), @@ -46,7 +48,8 @@ urlpatterns = [ path('nodes//tasks/', api.NodeTaskCreateApi.as_view(), name='node-task-create'), path('gateways//test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'), - path('platform-automation-methods/', api.PlatformAutomationMethodsApi.as_view(), name='platform-automation-methods'), + path('platform-automation-methods/', api.PlatformAutomationMethodsApi.as_view(), + name='platform-automation-methods'), ] urlpatterns += router.urls diff --git a/apps/audits/utils.py b/apps/audits/utils.py index 33e0d9639..2c54565d5 100644 --- a/apps/audits/utils.py +++ b/apps/audits/utils.py @@ -42,7 +42,7 @@ def _get_instance_field_value( if getattr(f, 'attname', None) in model_need_continue_fields: continue - value = getattr(instance, f.name) or getattr(instance, f.attname) + value = getattr(instance, f.name, None) or getattr(instance, f.attname, None) if not isinstance(value, bool) and not value: continue diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 23344c9fb..a98855f85 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -8,12 +8,13 @@ from django.http import HttpResponse from django.shortcuts import get_object_or_404 from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from rest_framework import status +from rest_framework import status, serializers from rest_framework.decorators import action -from rest_framework.exceptions import PermissionDenied +from rest_framework.exceptions import PermissionDenied, ValidationError from rest_framework.request import Request from rest_framework.response import Response +from accounts.const import AliasAccount from common.api import JMSModelViewSet from common.exceptions import JMSException from common.utils import random_string, get_logger, get_request_ip @@ -22,12 +23,12 @@ from common.utils.http import is_true, is_false from orgs.mixins.api import RootOrgViewMixin from perms.models import ActionChoices from terminal.connect_methods import NativeClient, ConnectMethodUtil -from terminal.models import EndpointRule +from terminal.models import EndpointRule, Endpoint from ..models import ConnectionToken, date_expired_default from ..serializers import ( ConnectionTokenSerializer, ConnectionTokenSecretSerializer, SuperConnectionTokenSerializer, ConnectTokenAppletOptionSerializer, - ConnectionTokenUpdateSerializer + ConnectionTokenReusableSerializer, ) __all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet'] @@ -165,11 +166,13 @@ class RDPFileClientProtocolURLMixin: return data def get_smart_endpoint(self, protocol, asset=None): - target_ip = asset.get_target_ip() if asset else '' - endpoint = EndpointRule.match_endpoint( - target_instance=asset, target_ip=target_ip, - protocol=protocol, request=self.request - ) + endpoint = Endpoint.match_by_instance_label(asset, protocol) + if not endpoint: + target_ip = asset.get_target_ip() if asset else '' + endpoint = EndpointRule.match_endpoint( + target_instance=asset, target_ip=target_ip, + protocol=protocol, request=self.request + ) return endpoint @@ -211,6 +214,18 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin): instance.expire() return Response(status=status.HTTP_204_NO_CONTENT) + @action(methods=['PATCH'], detail=True, url_path='reuse') + def reuse(self, request, *args, **kwargs): + instance = self.get_object() + if not settings.CONNECTION_TOKEN_REUSABLE: + error = _('Reusable connection token is not allowed, global setting not enabled') + raise serializers.ValidationError(error) + serializer = self.get_serializer(instance, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + is_reusable = serializer.validated_data.get('is_reusable', False) + instance.set_reusable(is_reusable) + return Response(data=serializer.data) + @action(methods=['POST'], detail=False) def exchange(self, request, *args, **kwargs): pk = request.data.get('id', None) or request.data.get('pk', None) @@ -231,17 +246,16 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView search_fields = filterset_fields serializer_classes = { 'default': ConnectionTokenSerializer, - 'update': ConnectionTokenUpdateSerializer, - 'partial_update': ConnectionTokenUpdateSerializer, + 'reuse': ConnectionTokenReusableSerializer, } http_method_names = ['get', 'post', 'patch', 'head', 'options', 'trace'] rbac_perms = { 'list': 'authentication.view_connectiontoken', 'retrieve': 'authentication.view_connectiontoken', - 'update': 'authentication.change_connectiontoken', 'create': 'authentication.add_connectiontoken', 'exchange': 'authentication.add_connectiontoken', - 'expire': 'authentication.change_connectiontoken', + 'reuse': 'authentication.reuse_connectiontoken', + 'expire': 'authentication.expire_connectiontoken', 'get_rdp_file': 'authentication.add_connectiontoken', 'get_client_protocol_url': 'authentication.add_connectiontoken', } @@ -282,13 +296,17 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView data['org_id'] = asset.org_id data['user'] = user data['value'] = random_string(16) + + if account_name == AliasAccount.ANON and asset.category not in ['web', 'custom']: + raise ValidationError(_('Anonymous account is not supported for this asset')) + account = self._validate_perm(user, asset, account_name) if account.has_secret: data['input_secret'] = '' - if account.username != '@INPUT': + if account.username != AliasAccount.INPUT: data['input_username'] = '' - if account.username == '@USER': + elif account.username == AliasAccount.USER: data['input_username'] = user.username ticket = self._validate_acl(user, asset, account) @@ -341,7 +359,7 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet): rbac_perms = { 'create': 'authentication.add_superconnectiontoken', 'renewal': 'authentication.add_superconnectiontoken', - 'get_secret_detail': 'authentication.view_connectiontokensecret', + 'get_secret_detail': 'authentication.view_superconnectiontokensecret', 'get_applet_info': 'authentication.view_superconnectiontoken', 'release_applet_account': 'authentication.view_superconnectiontoken', } @@ -371,7 +389,7 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet): @action(methods=['POST'], detail=False, url_path='secret') def get_secret_detail(self, request, *args, **kwargs): """ 非常重要的 api, 在逻辑层再判断一下 rbac 权限, 双重保险 """ - rbac_perm = 'authentication.view_connectiontokensecret' + rbac_perm = 'authentication.view_superconnectiontokensecret' if not request.user.has_perm(rbac_perm): raise PermissionDenied('Not allow to view secret') diff --git a/apps/authentication/api/password.py b/apps/authentication/api/password.py index da3e86510..53f47c03c 100644 --- a/apps/authentication/api/password.py +++ b/apps/authentication/api/password.py @@ -1,3 +1,4 @@ +from django.http import HttpResponseRedirect from rest_framework.generics import CreateAPIView from rest_framework.response import Response from rest_framework.permissions import AllowAny @@ -41,7 +42,7 @@ class UserResetPasswordSendCodeApi(CreateAPIView): token = request.GET.get('token') userinfo = cache.get(token) if not userinfo: - return reverse('authentication:forgot-previewing') + return HttpResponseRedirect(reverse('authentication:forgot-previewing')) serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) diff --git a/apps/authentication/backends/ldap.py b/apps/authentication/backends/ldap.py index 354d6211e..616052af2 100644 --- a/apps/authentication/backends/ldap.py +++ b/apps/authentication/backends/ldap.py @@ -9,6 +9,7 @@ from django_auth_ldap.config import _LDAPConfig, LDAPSearch, LDAPSearchUnion from users.utils import construct_user_email from common.const import LDAP_AD_ACCOUNT_DISABLE +from common.utils.http import is_true from .base import JMSBaseAuthBackend logger = _LDAPConfig.get_logger() @@ -162,10 +163,11 @@ class LDAPUser(_LDAPUser): try: value = self.attrs[attr][0] value = value.strip() - if attr.lower() == 'useraccountcontrol' \ - and field == 'is_active' and value: - value = int(value) & LDAP_AD_ACCOUNT_DISABLE \ - != LDAP_AD_ACCOUNT_DISABLE + if field == 'is_active': + if attr.lower() == 'useraccountcontrol' and value: + value = int(value) & LDAP_AD_ACCOUNT_DISABLE != LDAP_AD_ACCOUNT_DISABLE + else: + value = is_true(value) except LookupError: logger.warning("{} does not have a value for the attribute {}".format(self.dn, attr)) else: diff --git a/apps/authentication/migrations/0021_auto_20230713_1459.py b/apps/authentication/migrations/0021_auto_20230713_1459.py new file mode 100644 index 000000000..03913f766 --- /dev/null +++ b/apps/authentication/migrations/0021_auto_20230713_1459.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.19 on 2023-07-13 06:59 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ('authentication', '0020_connectiontoken_connect_options'), + ] + + operations = [ + migrations.AlterModelOptions( + name='connectiontoken', + options={'ordering': ('-date_expired',), + 'permissions': [('expire_connectiontoken', 'Can expire connection token'), + ('reuse_connectiontoken', 'Can reuse connection token')], + 'verbose_name': 'Connection token'}, + ), + migrations.AlterModelOptions( + name='superconnectiontoken', + options={'permissions': [('view_superconnectiontokensecret', 'Can view super connection token secret')], + 'verbose_name': 'Super connection token'}, + ), + ] diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py index 0407cf025..ce5b4829f 100644 --- a/apps/authentication/models/connection_token.py +++ b/apps/authentication/models/connection_token.py @@ -9,6 +9,7 @@ from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from rest_framework.exceptions import PermissionDenied +from accounts.const import AliasAccount from assets.const import Protocol from assets.const.host import GATEWAY_NAME from common.db.fields import EncryptTextField @@ -21,7 +22,7 @@ from terminal.models import Applet def date_expired_default(): - return timezone.now() + timedelta(seconds=settings.CONNECTION_TOKEN_EXPIRATION) + return timezone.now() + timedelta(seconds=settings.CONNECTION_TOKEN_ONETIME_EXPIRATION) class ConnectionToken(JMSOrgBaseModel): @@ -53,10 +54,11 @@ class ConnectionToken(JMSOrgBaseModel): class Meta: ordering = ('-date_expired',) - verbose_name = _('Connection token') permissions = [ - ('view_connectiontokensecret', _('Can view connection token secret')) + ('expire_connectiontoken', _('Can expire connection token')), + ('reuse_connectiontoken', _('Can reuse connection token')), ] + verbose_name = _('Connection token') @property def is_expired(self): @@ -79,6 +81,15 @@ class ConnectionToken(JMSOrgBaseModel): self.date_expired = timezone.now() self.save(update_fields=['date_expired']) + def set_reusable(self, is_reusable): + self.is_reusable = is_reusable + if self.is_reusable: + seconds = settings.CONNECTION_TOKEN_REUSABLE_EXPIRATION + else: + seconds = settings.CONNECTION_TOKEN_ONETIME_EXPIRATION + self.date_expired = timezone.now() + timedelta(seconds=seconds) + self.save(update_fields=['is_reusable', 'date_expired']) + def renewal(self): """ 续期 Token,将来支持用户自定义创建 token 后,续期策略要修改 """ self.date_expired = date_expired_default() @@ -175,7 +186,7 @@ class ConnectionToken(JMSOrgBaseModel): if not applet: return None - host_account = applet.select_host_account(self.user) + host_account = applet.select_host_account(self.user, self.asset) if not host_account: raise JMSException({'error': 'No host account available'}) @@ -209,29 +220,19 @@ class ConnectionToken(JMSOrgBaseModel): if not self.asset: return None - account = self.asset.accounts.filter(name=self.account).first() - if self.account == '@INPUT' or not account: - data = { - 'name': self.account, - 'username': self.input_username, - 'secret_type': 'password', - 'secret': self.input_secret, - 'su_from': None, - 'org_id': self.asset.org_id, - 'asset': self.asset - } + if self.account.startswith('@'): + account = Account.get_special_account(self.account) + account.asset = self.asset + account.org_id = self.asset.org_id + + if self.account in [AliasAccount.INPUT, AliasAccount.USER]: + account.username = self.input_username + account.secret = self.input_secret else: - data = { - 'name': account.name, - 'username': account.username, - 'secret_type': account.secret_type, - 'secret': account.secret or self.input_secret, - 'su_from': account.su_from, - 'org_id': account.org_id, - 'privileged': account.privileged, - 'asset': self.asset - } - return Account(**data) + account = self.asset.accounts.filter(name=self.account).first() + if not account.secret and self.input_secret: + account.secret = self.input_secret + return account @lazyproperty def domain(self): @@ -264,4 +265,7 @@ class ConnectionToken(JMSOrgBaseModel): class SuperConnectionToken(ConnectionToken): class Meta: proxy = True + permissions = [ + ('view_superconnectiontokensecret', _('Can view super connection token secret')) + ] verbose_name = _("Super connection token") diff --git a/apps/authentication/serializers/connection_token.py b/apps/authentication/serializers/connection_token.py index 74b645aec..bc3d051e1 100644 --- a/apps/authentication/serializers/connection_token.py +++ b/apps/authentication/serializers/connection_token.py @@ -1,20 +1,18 @@ -from django.conf import settings -from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers +from common.serializers import CommonModelSerializer from common.serializers.fields import EncryptedField -from orgs.mixins.serializers import OrgResourceModelSerializerMixin from perms.serializers.permission import ActionChoicesField from ..models import ConnectionToken __all__ = [ 'ConnectionTokenSerializer', 'SuperConnectionTokenSerializer', - 'ConnectionTokenUpdateSerializer', + 'ConnectionTokenReusableSerializer', ] -class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): +class ConnectionTokenSerializer(CommonModelSerializer): 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,30 +58,12 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): return info -class ConnectionTokenUpdateSerializer(ConnectionTokenSerializer): - class Meta(ConnectionTokenSerializer.Meta): +class ConnectionTokenReusableSerializer(CommonModelSerializer): + class Meta: + model = ConnectionToken + fields = ['id', 'date_expired', 'is_reusable'] can_update_fields = ['is_reusable'] - read_only_fields = list(set(ConnectionTokenSerializer.Meta.fields) - set(can_update_fields)) - - def _get_date_expired(self): - delta = self.instance.date_expired - self.instance.date_created - if delta.total_seconds() > 3600 * 24: - return self.instance.date_expired - - seconds = settings.CONNECTION_TOKEN_EXPIRATION_MAX - return timezone.now() + timezone.timedelta(seconds=seconds) - - @staticmethod - def validate_is_reusable(value): - if value and not settings.CONNECTION_TOKEN_REUSABLE: - raise serializers.ValidationError(_('Reusable connection token is not allowed, global setting not enabled')) - return value - - def validate(self, attrs): - reusable = attrs.get('is_reusable', False) - if reusable: - attrs['date_expired'] = self._get_date_expired() - return attrs + read_only_fields = list(set(fields) - set(can_update_fields)) class SuperConnectionTokenSerializer(ConnectionTokenSerializer): diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index dfba7155c..8b5696dc9 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -2,15 +2,19 @@ # from __future__ import unicode_literals -import os + import datetime +import os from typing import Callable -from django.db import IntegrityError -from django.templatetags.static import static +from django.conf import settings +from django.contrib.auth import BACKEND_SESSION_KEY from django.contrib.auth import login as auth_login, logout as auth_logout -from django.http import HttpResponse, HttpRequest +from django.db import IntegrityError +from django.http import HttpRequest from django.shortcuts import reverse, redirect +from django.templatetags.static import static +from django.urls import reverse_lazy from django.utils.decorators import method_decorator from django.utils.translation import ugettext as _, get_language from django.views.decorators.cache import never_cache @@ -18,16 +22,13 @@ from django.views.decorators.csrf import csrf_protect from django.views.decorators.debug import sensitive_post_parameters from django.views.generic.base import TemplateView, RedirectView from django.views.generic.edit import FormView -from django.conf import settings -from django.urls import reverse_lazy -from django.contrib.auth import BACKEND_SESSION_KEY from common.utils import FlashMessageUtil, static_or_direct from users.utils import ( redirect_user_first_login_or_index ) -from ..const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY from .. import mixins, errors +from ..const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY from ..forms import get_user_login_form_cls __all__ = [ @@ -203,7 +204,9 @@ class UserLoginView(mixins.AuthMixin, UserLoginContextMixin, FormView): def form_valid(self, form): if not self.request.session.test_cookie_worked(): - return HttpResponse(_("Please enable cookies and try again.")) + form.add_error(None, _("Login timeout, please try again.")) + return self.form_invalid(form) + # https://docs.djangoproject.com/en/3.1/topics/http/sessions/#setting-test-cookies self.request.session.delete_test_cookie() diff --git a/apps/common/api/filter.py b/apps/common/api/filter.py index c0307dd59..56c3e6483 100644 --- a/apps/common/api/filter.py +++ b/apps/common/api/filter.py @@ -10,6 +10,8 @@ from common.drf.filters import IDSpmFilter, CustomFilter, IDInFilter __all__ = ['ExtraFilterFieldsMixin', 'OrderingFielderFieldsMixin'] +logger = logging.getLogger('jumpserver.common') + class ExtraFilterFieldsMixin: """ @@ -54,7 +56,9 @@ class OrderingFielderFieldsMixin: try: valid_fields = self.get_valid_ordering_fields() except Exception as e: - logging.debug('get_valid_ordering_fields error: %s' % e) + logger.debug('get_valid_ordering_fields error: %s' % e) + # 这里千万不要这么用,会让 logging 重复,至于为什么,我也不知道 + # logging.debug('get_valid_ordering_fields error: %s' % e) valid_fields = [] fields = list(chain( diff --git a/apps/common/auth/signature.py b/apps/common/auth/signature.py index 793d2a639..9661b9620 100644 --- a/apps/common/auth/signature.py +++ b/apps/common/auth/signature.py @@ -40,7 +40,7 @@ class SignatureAuthentication(authentication.BaseAuthentication): required_headers = ["(request-target)", "date"] def fetch_user_data(self, key_id, algorithm=None): - """Retuns a tuple (User, secret) or (None, None).""" + """Returns a tuple (User, secret) or (None, None).""" raise NotImplementedError() def authenticate_header(self, request): diff --git a/apps/common/db/fields.py b/apps/common/db/fields.py index 5b7ee7c5f..dee051da5 100644 --- a/apps/common/db/fields.py +++ b/apps/common/db/fields.py @@ -328,13 +328,13 @@ class RelatedManager: q = Q() if isinstance(val, str): val = [val] + if ['*'] in val: + return Q() for ip in val: if not ip: continue try: - if ip == '*': - return Q() - elif '/' in ip: + if '/' in ip: network = ipaddress.ip_network(ip) ips = network.hosts() q |= Q(**{"{}__in".format(name): ips}) @@ -378,7 +378,7 @@ class RelatedManager: if match == 'ip_in': q = cls.get_ip_in_q(name, val) - elif match in ("exact", "contains", "startswith", "endswith", "gte", "lte", "gt", "lt"): + elif match in ("contains", "startswith", "endswith", "gte", "lte", "gt", "lt"): lookup = "{}__{}".format(name, match) q = Q(**{lookup: val}) elif match == 'regex': @@ -387,7 +387,7 @@ class RelatedManager: lookup = "{}__{}".format(name, match) q = Q(**{lookup: val}) except re.error: - q = ~Q() + q = Q(pk__isnull=True) elif match == "not": q = ~Q(**{name: val}) elif match in ['m2m', 'in']: @@ -459,7 +459,7 @@ class JSONManyToManyDescriptor: custom_q = Q() for rule in attr_rules: - value = getattr(obj, rule['name'], '') + value = getattr(obj, rule['name'], None) or '' rule_value = rule.get('value', '') rule_match = rule.get('match', 'exact') @@ -470,11 +470,11 @@ class JSONManyToManyDescriptor: continue if rule_match == 'in': - res &= value in rule_value + res &= value in rule_value or '*' in rule_value elif rule_match == 'exact': - res &= value == rule_value + res &= value == rule_value or rule_value == '*' elif rule_match == 'contains': - res &= rule_value in value + res &= (rule_value in value) elif rule_match == 'startswith': res &= str(value).startswith(str(rule_value)) elif rule_match == 'endswith': @@ -499,7 +499,7 @@ class JSONManyToManyDescriptor: elif rule['match'] == 'ip_in': if isinstance(rule_value, str): rule_value = [rule_value] - res &= contains_ip(value, rule_value) + res &= '*' in rule_value or contains_ip(value, rule_value) elif rule['match'] == 'm2m': if isinstance(value, Manager): value = value.values_list('id', flat=True) diff --git a/apps/common/decorators.py b/apps/common/decorators.py index c100c5025..4881c1517 100644 --- a/apps/common/decorators.py +++ b/apps/common/decorators.py @@ -6,6 +6,7 @@ import inspect import threading import time from concurrent.futures import ThreadPoolExecutor +from functools import wraps from django.db import transaction @@ -217,3 +218,24 @@ def do_test(): end = time.time() using = end - s print("end : %s, using: %s" % (end, using)) + + +def cached_method(ttl=20): + _cache = {} + + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + key = (func, args, tuple(sorted(kwargs.items()))) + # 检查缓存是否存在且未过期 + if key in _cache and time.time() - _cache[key]['timestamp'] < ttl: + return _cache[key]['result'] + + # 缓存过期或不存在,执行方法并缓存结果 + result = func(*args, **kwargs) + _cache[key] = {'result': result, 'timestamp': time.time()} + return result + + return wrapper + + return decorator diff --git a/apps/common/drf/filters.py b/apps/common/drf/filters.py index bac14190e..c44874fd5 100644 --- a/apps/common/drf/filters.py +++ b/apps/common/drf/filters.py @@ -14,6 +14,8 @@ from rest_framework.serializers import ValidationError from common import const +logger = logging.getLogger('jumpserver.common') + __all__ = [ "DatetimeRangeFilter", "IDSpmFilter", 'IDInFilter', "CustomFilter", @@ -70,7 +72,7 @@ class DatetimeRangeFilter(filters.BaseFilterBackend): ] ``` """.format(view.name) - logging.error(msg) + logger.error(msg) raise ImproperlyConfigured(msg) def filter_queryset(self, request, queryset, view): @@ -213,6 +215,6 @@ class AttrRulesFilterBackend(filters.BaseFilterBackend): except Exception: raise ValidationError({'attr_rules': 'attr_rules should be json'}) - logging.debug('attr_rules: %s', attr_rules) + logger.debug('attr_rules: %s', attr_rules) q = RelatedManager.get_to_filter_q(attr_rules, queryset.model) return queryset.filter(q).distinct() diff --git a/apps/common/drf/parsers/base.py b/apps/common/drf/parsers/base.py index 9ba77ca72..43d736f21 100644 --- a/apps/common/drf/parsers/base.py +++ b/apps/common/drf/parsers/base.py @@ -52,14 +52,16 @@ class BaseFileParser(BaseParser): fields_map = {} fields = self.serializer_fields for k, v in fields.items(): - if v.read_only: + # 资产平台的 id 是只读的, 导入更新资产平台会失败 + if v.read_only and k not in ['id', 'pk']: continue fields_map.update({ v.label: k, k: k }) + lowercase_fields_map = {k.lower(): v for k, v in fields_map.items()} field_names = [ - fields_map.get(column_title.strip('*'), '') + lowercase_fields_map.get(column_title.strip('*').lower(), '') for column_title in column_titles ] return field_names diff --git a/apps/common/management/commands/services/command.py b/apps/common/management/commands/services/command.py index fcaa8f1cd..487d9ce5f 100644 --- a/apps/common/management/commands/services/command.py +++ b/apps/common/management/commands/services/command.py @@ -1,8 +1,10 @@ import multiprocessing -from django.core.management.base import BaseCommand, CommandError + +from django.core.management.base import BaseCommand from django.db.models import TextChoices -from .utils import ServicesUtil + from .hands import * +from .utils import ServicesUtil class Services(TextChoices): @@ -92,15 +94,11 @@ class BaseActionCommand(BaseCommand): super().__init__(*args, **kwargs) def add_arguments(self, parser): - cores = 10 - if (multiprocessing.cpu_count() * 2 + 1) < cores: - cores = multiprocessing.cpu_count() * 2 + 1 - parser.add_argument( - 'services', nargs='+', choices=Services.export_services_values(), help='Service', + 'services', nargs='+', choices=Services.export_services_values(), help='Service', ) parser.add_argument('-d', '--daemon', nargs="?", const=True) - parser.add_argument('-w', '--worker', type=int, nargs="?", default=cores) + parser.add_argument('-w', '--worker', type=int, nargs="?", default=4) parser.add_argument('-f', '--force', nargs="?", const=True) def initial_util(self, *args, **options): diff --git a/apps/common/management/commands/services/hands.py b/apps/common/management/commands/services/hands.py index eb6aba418..c2e625e8b 100644 --- a/apps/common/management/commands/services/hands.py +++ b/apps/common/management/commands/services/hands.py @@ -1,12 +1,14 @@ +import logging import os import sys -import logging + from django.conf import settings from apps.jumpserver.const import CONFIG try: from apps.jumpserver import const + __version__ = const.VERSION except ImportError as e: print("Not found __version__: {}".format(e)) @@ -15,12 +17,11 @@ except ImportError as e: __version__ = 'Unknown' sys.exit(1) - HTTP_HOST = CONFIG.HTTP_BIND_HOST or '127.0.0.1' HTTP_PORT = CONFIG.HTTP_LISTEN_PORT or 8080 WS_PORT = CONFIG.WS_LISTEN_PORT or 8082 DEBUG = CONFIG.DEBUG or False BASE_DIR = os.path.dirname(settings.BASE_DIR) -LOG_DIR = os.path.join(BASE_DIR, 'logs') +LOG_DIR = os.path.join(BASE_DIR, 'data', 'logs') APPS_DIR = os.path.join(BASE_DIR, 'apps') TMP_DIR = os.path.join(BASE_DIR, 'tmp') diff --git a/apps/common/management/commands/services/services/gunicorn.py b/apps/common/management/commands/services/services/gunicorn.py index 5eab30ec3..66aa9f7bc 100644 --- a/apps/common/management/commands/services/services/gunicorn.py +++ b/apps/common/management/commands/services/services/gunicorn.py @@ -1,5 +1,5 @@ -from ..hands import * from .base import BaseService +from ..hands import * __all__ = ['GunicornService'] @@ -22,7 +22,8 @@ class GunicornService(BaseService): '-b', bind, '-k', 'uvicorn.workers.UvicornWorker', '-w', str(self.worker), - '--max-requests', '4096', + '--max-requests', '10240', + '--max-requests-jitter', '2048', '--access-logformat', log_format, '--access-logfile', '-' ] diff --git a/apps/common/serializers/dynamic.py b/apps/common/serializers/dynamic.py index cac9ae0dc..a42529b5a 100644 --- a/apps/common/serializers/dynamic.py +++ b/apps/common/serializers/dynamic.py @@ -44,19 +44,24 @@ def set_default_by_type(tp, data, field_info): def create_serializer_class(serializer_name, fields_info): serializer_fields = {} - fields_name = ['name', 'label', 'default', 'type', 'help_text'] + fields_name = ['name', 'label', 'default', 'required', 'type', 'help_text'] for i, field_info in enumerate(fields_info): data = {k: field_info.get(k) for k in fields_name} field_type = data.pop('type', 'str') - if data.get('default') is None: + # 用户定义 default 和 required 可能会冲突, 所以要处理一下 + default = data.get('default', None) + if default is None: data.pop('default', None) - data['required'] = field_info.get('required', True) + data['required'] = True + elif default == '': + data['required'] = False + data['allow_blank'] = True + else: + data['required'] = False data = set_default_by_type(field_type, data, field_info) data = set_default_if_need(data, i) - if data.get('default', None) is not None: - data['required'] = False field_name = data.pop('name') field_class = type_field_map.get(field_type, serializers.CharField) serializer_fields[field_name] = field_class(**data) diff --git a/apps/common/serializers/fields.py b/apps/common/serializers/fields.py index cf92391e1..844c424e5 100644 --- a/apps/common/serializers/fields.py +++ b/apps/common/serializers/fields.py @@ -212,6 +212,23 @@ class BitChoicesField(TreeChoicesField): class PhoneField(serializers.CharField): + + def to_internal_value(self, data): + if isinstance(data, dict): + code = data.get('code') + phone = data.get('phone', '') + if code and phone: + data = '{}{}'.format(code, phone) + else: + data = phone + try: + phone = phonenumbers.parse(data, 'CN') + data = '{}{}'.format(phone.country_code, phone.national_number) + except phonenumbers.NumberParseException: + data = '+86{}'.format(data) + + return super().to_internal_value(data) + def to_representation(self, value): if value: try: diff --git a/apps/common/signal_handlers.py b/apps/common/signal_handlers.py index b7acf6e79..df2019b60 100644 --- a/apps/common/signal_handlers.py +++ b/apps/common/signal_handlers.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- # -import logging import os import re from collections import defaultdict @@ -14,9 +13,10 @@ from django.dispatch import receiver from jumpserver.utils import get_current_request from .local import thread_local from .signals import django_ready +from .utils import get_logger pattern = re.compile(r'FROM `(\w+)`') -logger = logging.getLogger("jumpserver.common") +logger = get_logger(__name__) class Counter: @@ -129,7 +129,6 @@ else: @receiver(django_ready) def check_migrations_file_prefix_conflict(*args, **kwargs): - if not settings.DEBUG_DEV: return @@ -172,7 +171,7 @@ def check_migrations_file_prefix_conflict(*args, **kwargs): if not conflict_count: return - print('='*80) + print('=' * 80) for conflict_file in conflict_files: msg_dir = '{:<15}'.format(conflict_file[0]) msg_split = '=> ' @@ -181,4 +180,4 @@ def check_migrations_file_prefix_conflict(*args, **kwargs): msg_right2 = ' ' * len(msg_left) + msg_split + conflict_file[2] print(f'{msg_left}{msg_right1}\n{msg_right2}\n') - print('='*80) + print('=' * 80) diff --git a/apps/common/tasks.py b/apps/common/tasks.py index ee4880ee7..a3130ead6 100644 --- a/apps/common/tasks.py +++ b/apps/common/tasks.py @@ -49,6 +49,7 @@ def send_mail_attachment_async(subject, message, recipient_list, attachment_list if attachment_list is None: attachment_list = [] from_email = settings.EMAIL_FROM or settings.EMAIL_HOST_USER + subject = (settings.EMAIL_SUBJECT_PREFIX or '') + subject email = EmailMultiAlternatives( subject=subject, body=message, diff --git a/apps/common/utils/verify_code.py b/apps/common/utils/verify_code.py index 0dbf810fd..acd547296 100644 --- a/apps/common/utils/verify_code.py +++ b/apps/common/utils/verify_code.py @@ -1,15 +1,15 @@ -from django.core.cache import cache -from django.conf import settings -from django.core.mail import send_mail from celery import shared_task - -from common.sdk.sms.exceptions import CodeError, CodeExpired, CodeSendTooFrequently -from common.sdk.sms.endpoint import SMS -from common.exceptions import JMSException -from common.utils.random import random_string -from common.utils import get_logger +from django.conf import settings +from django.core.cache import cache from django.utils.translation import gettext_lazy as _ +from common.exceptions import JMSException +from common.sdk.sms.endpoint import SMS +from common.sdk.sms.exceptions import CodeError, CodeExpired, CodeSendTooFrequently +from common.tasks import send_mail_async +from common.utils import get_logger +from common.utils.random import random_string + logger = get_logger(__file__) @@ -78,8 +78,7 @@ class SendAndVerifyCodeUtil(object): def __send_with_email(self): subject = self.other_args.get('subject') message = self.other_args.get('message') - from_email = settings.EMAIL_FROM or settings.EMAIL_HOST_USER - send_mail(subject, message, from_email, [self.target], html_message=message) + send_mail_async(subject, message, [self.target], html_message=message) def __send(self, code): """ diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py index b5ebd9ebc..3b72841f9 100644 --- a/apps/jumpserver/api.py +++ b/apps/jumpserver/api.py @@ -17,9 +17,9 @@ from audits.models import UserLoginLog, PasswordChangeLog, OperateLog, FTPLog, J from common.utils import lazyproperty from common.utils.timezone import local_now, local_zero_hour from ops.const import JobStatus -from ops.models import JobExecution from orgs.caches import OrgResourceStatisticsCache from orgs.utils import current_org +from terminal.const import RiskLevelChoices from terminal.models import Session, Command from terminal.utils import ComponentsPrometheusMetricsUtil from users.models import User @@ -50,6 +50,10 @@ class DateTimeMixin: t = local_now() - timezone.timedelta(days=days) return t + @lazyproperty + def date_start_end(self): + return self.days_to_datetime.date(), local_now().date() + @lazyproperty def dates_list(self): now = local_now() @@ -126,12 +130,6 @@ class DateTimeMixin: queryset = JobLog.objects.filter(date_created__gte=t) return queryset - @lazyproperty - def jobs_executed_queryset(self): - t = self.days_to_datetime - queryset = JobExecution.objects.filter(date_created__gte=t) - return queryset - class DatesLoginMetricMixin: dates_list: list @@ -143,101 +141,40 @@ class DatesLoginMetricMixin: operate_logs_queryset: OperateLog.objects password_change_logs_queryset: PasswordChangeLog.objects - @staticmethod - def get_cache_key(date, tp): - date_str = date.strftime("%Y%m%d") - key = "SESSION_DATE_{}_{}_{}".format(current_org.id, tp, date_str) - return key - - def __get_data_from_cache(self, date, tp): - if date == timezone.now().date(): - return None - cache_key = self.get_cache_key(date, tp) - count = cache.get(cache_key) - return count - - def __set_data_to_cache(self, date, tp, count): - cache_key = self.get_cache_key(date, tp) - cache.set(cache_key, count, 3600) - - @staticmethod - def get_date_start_2_end(d): - time_min = timezone.datetime.min.time() - time_max = timezone.datetime.max.time() - tz = timezone.get_current_timezone() - ds = timezone.datetime.combine(d, time_min).replace(tzinfo=tz) - de = timezone.datetime.combine(d, time_max).replace(tzinfo=tz) - return ds, de - - def get_date_login_count(self, date): - tp = "LOGIN-USER" - count = self.__get_data_from_cache(date, tp) - if count is not None: - return count - ds, de = self.get_date_start_2_end(date) - count = UserLoginLog.objects.filter(datetime__range=(ds, de)).count() - self.__set_data_to_cache(date, tp, count) - return count - def get_dates_metrics_total_count_login(self): - data = [] - for d in self.dates_list: - count = self.get_date_login_count(d) - data.append(count) - if len(data) == 0: - data = [0] - return data - - def get_date_user_count(self, date): - tp = "USER" - count = self.__get_data_from_cache(date, tp) - if count is not None: - return count - ds, de = self.get_date_start_2_end(date) - count = len(set(Session.objects.filter(date_start__range=(ds, de)).values_list('user_id', flat=True))) - self.__set_data_to_cache(date, tp, count) - return count + queryset = UserLoginLog.objects \ + .filter(datetime__range=(self.date_start_end)) \ + .values('datetime__date').annotate(id__count=Count(id)) \ + .order_by('datetime__date') + map_date_logincount = {i['datetime__date']: i['id__count'] for i in queryset} + return [map_date_logincount.get(d, 0) for d in self.dates_list] def get_dates_metrics_total_count_active_users(self): - data = [] - for d in self.dates_list: - count = self.get_date_user_count(d) - data.append(count) - return data - - def get_date_asset_count(self, date): - tp = "ASSET" - count = self.__get_data_from_cache(date, tp) - if count is not None: - return count - ds, de = self.get_date_start_2_end(date) - count = len(set(Session.objects.filter(date_start__range=(ds, de)).values_list('asset', flat=True))) - self.__set_data_to_cache(date, tp, count) - return count + queryset = Session.objects \ + .filter(date_start__range=(self.date_start_end)) \ + .values('date_start__date') \ + .annotate(id__count=Count('user_id', distinct=True)) \ + .order_by('date_start__date') + map_date_usercount = {i['date_start__date']: i['id__count'] for i in queryset} + return [map_date_usercount.get(d, 0) for d in self.dates_list] def get_dates_metrics_total_count_active_assets(self): - data = [] - for d in self.dates_list: - count = self.get_date_asset_count(d) - data.append(count) - return data - - def get_date_session_count(self, date): - tp = "SESSION" - count = self.__get_data_from_cache(date, tp) - if count is not None: - return count - ds, de = self.get_date_start_2_end(date) - count = Session.objects.filter(date_start__range=(ds, de)).count() - self.__set_data_to_cache(date, tp, count) - return count + queryset = Session.objects \ + .filter(date_start__range=(self.date_start_end)) \ + .values('date_start__date') \ + .annotate(id__count=Count('asset_id', distinct=True)) \ + .order_by('date_start__date') + map_date_assetcount = {i['date_start__date']: i['id__count'] for i in queryset} + return [map_date_assetcount.get(d, 0) for d in self.dates_list] def get_dates_metrics_total_count_sessions(self): - data = [] - for d in self.dates_list: - count = self.get_date_session_count(d) - data.append(count) - return data + queryset = Session.objects \ + .filter(date_start__range=(self.date_start_end)) \ + .values('date_start__date') \ + .annotate(id__count=Count(id)) \ + .order_by('date_start__date') + map_date_usercount = {i['date_start__date']: i['id__count'] for i in queryset} + return [map_date_usercount.get(d, 0) for d in self.dates_list] @lazyproperty def get_type_to_assets(self): @@ -312,7 +249,7 @@ class DatesLoginMetricMixin: @lazyproperty def commands_danger_amount(self): - return self.command_queryset.filter(risk_level=Command.RiskLevelChoices.dangerous).count() + return self.command_queryset.filter(risk_level=RiskLevelChoices.reject).count() @lazyproperty def job_logs_running_amount(self): diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index dd056b091..33f0dbaf4 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -186,8 +186,9 @@ class Config(dict): 'BOOTSTRAP_TOKEN': '', 'DEBUG': False, 'DEBUG_DEV': False, + 'DEBUG_ANSIBLE': False, 'LOG_LEVEL': 'DEBUG', - 'LOG_DIR': os.path.join(PROJECT_DIR, 'logs'), + 'LOG_DIR': os.path.join(PROJECT_DIR, 'data', 'logs'), 'DB_ENGINE': 'mysql', 'DB_NAME': 'jumpserver', 'DB_HOST': '127.0.0.1', @@ -231,8 +232,12 @@ class Config(dict): 'SESSION_COOKIE_AGE': 3600 * 24, 'SESSION_EXPIRE_AT_BROWSER_CLOSE': False, 'LOGIN_URL': reverse_lazy('authentication:login'), - 'CONNECTION_TOKEN_EXPIRATION': 5 * 60, # 默认 - 'CONNECTION_TOKEN_EXPIRATION_MAX': 60 * 60 * 24 * 30, # 最大 + + 'CONNECTION_TOKEN_ONETIME_EXPIRATION': 5 * 60, # 默认(new) + 'CONNECTION_TOKEN_EXPIRATION': 5 * 60, # 默认(old) + + 'CONNECTION_TOKEN_REUSABLE_EXPIRATION': 60 * 60 * 24 * 30, # 最大(new) + 'CONNECTION_TOKEN_EXPIRATION_MAX': 60 * 60 * 24 * 30, # 最大(old) 'CONNECTION_TOKEN_REUSABLE': False, # Custom Config @@ -558,6 +563,11 @@ class Config(dict): 'FTP_FILE_MAX_STORE': 100, } + old_config_map = { + 'CONNECTION_TOKEN_ONETIME_EXPIRATION': 'CONNECTION_TOKEN_EXPIRATION', + 'CONNECTION_TOKEN_REUSABLE_EXPIRATION': 'CONNECTION_TOKEN_EXPIRATION_MAX', + } + def __init__(self, *args): super().__init__(*args) self.secret_encryptor = ConfigCrypto.get_secret_encryptor() @@ -698,13 +708,19 @@ class Config(dict): value = self.convert_type(item, value) return value - def get(self, item): + def get(self, item, default=None): # 再从配置文件中获取 value = self.get_from_config(item) if value is None: value = self.get_from_env(item) + + # 因为要递归,所以优先从上次返回的递归中获取 + if default is None: + default = self.defaults.get(item) + if value is None and item in self.old_config_map: + return self.get(self.old_config_map[item], default) if value is None: - value = self.defaults.get(item) + value = default if self.secret_encryptor: value = self.secret_encryptor.decrypt_if_need(value, item) return value diff --git a/apps/jumpserver/middleware.py b/apps/jumpserver/middleware.py index f76077b7f..a4738649f 100644 --- a/apps/jumpserver/middleware.py +++ b/apps/jumpserver/middleware.py @@ -101,6 +101,19 @@ class RefererCheckMiddleware: return response +class SQLCountMiddleware: + def __init__(self, get_response): + self.get_response = get_response + if not settings.DEBUG_DEV: + raise MiddlewareNotUsed + + def __call__(self, request): + from django.db import connection + response = self.get_response(request) + response['X-JMS-SQL-COUNT'] = len(connection.queries) - 2 + return response + + class StartMiddleware: def __init__(self, get_response): self.get_response = get_response diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 2a45555bd..fa3217f50 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -175,13 +175,9 @@ AUTH_OAUTH2_LOGOUT_URL_NAME = "authentication:oauth2:logout" AUTH_TEMP_TOKEN = CONFIG.AUTH_TEMP_TOKEN # Other setting +# 这个是 User Login Private Token TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION OTP_IN_RADIUS = CONFIG.OTP_IN_RADIUS -# Connection token -CONNECTION_TOKEN_EXPIRATION = CONFIG.CONNECTION_TOKEN_EXPIRATION -if CONNECTION_TOKEN_EXPIRATION < 5 * 60: - # 最少5分钟 - CONNECTION_TOKEN_EXPIRATION = 5 * 60 RBAC_BACKEND = 'rbac.backends.RBACBackend' AUTH_BACKEND_MODEL = 'authentication.backends.base.JMSModelBackend' diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index e956dbd69..2bc547e57 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -53,6 +53,8 @@ BOOTSTRAP_TOKEN = CONFIG.BOOTSTRAP_TOKEN DEBUG = CONFIG.DEBUG # SECURITY WARNING: If you run with debug turned on, more debug msg with be log DEBUG_DEV = CONFIG.DEBUG_DEV +# SECURITY WARNING: If you run ansible task with debug turned on, more debug msg with be log +DEBUG_ANSIBLE = CONFIG.DEBUG_ANSIBLE # Absolute url for some case, for example email link SITE_URL = CONFIG.SITE_URL @@ -128,6 +130,7 @@ MIDDLEWARE = [ 'jumpserver.middleware.DemoMiddleware', 'jumpserver.middleware.RequestMiddleware', 'jumpserver.middleware.RefererCheckMiddleware', + 'jumpserver.middleware.SQLCountMiddleware', 'orgs.middleware.OrgMiddleware', 'authentication.backends.oidc.middleware.OIDCRefreshIDTokenMiddleware', 'authentication.backends.cas.middleware.CASMiddleware', diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index 78298db25..b9be9c75b 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -133,8 +133,13 @@ TICKETS_ENABLED = CONFIG.TICKETS_ENABLED REFERER_CHECK_ENABLED = CONFIG.REFERER_CHECK_ENABLED CONNECTION_TOKEN_ENABLED = CONFIG.CONNECTION_TOKEN_ENABLED +# Connection token +CONNECTION_TOKEN_ONETIME_EXPIRATION = CONFIG.CONNECTION_TOKEN_ONETIME_EXPIRATION +if CONNECTION_TOKEN_ONETIME_EXPIRATION < 5 * 60: + # 最少5分钟 + CONNECTION_TOKEN_ONETIME_EXPIRATION = 5 * 60 CONNECTION_TOKEN_REUSABLE = CONFIG.CONNECTION_TOKEN_REUSABLE -CONNECTION_TOKEN_EXPIRATION_MAX = CONFIG.CONNECTION_TOKEN_EXPIRATION_MAX +CONNECTION_TOKEN_REUSABLE_EXPIRATION = CONFIG.CONNECTION_TOKEN_REUSABLE_EXPIRATION FORGOT_PASSWORD_URL = CONFIG.FORGOT_PASSWORD_URL diff --git a/apps/jumpserver/settings/logging.py b/apps/jumpserver/settings/logging.py index a021d1716..c4cf90ebb 100644 --- a/apps/jumpserver/settings/logging.py +++ b/apps/jumpserver/settings/logging.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- # import os + from ..const import PROJECT_DIR, CONFIG -LOG_DIR = os.path.join(PROJECT_DIR, 'logs') +LOG_DIR = os.path.join(PROJECT_DIR, 'data', 'logs') JUMPSERVER_LOG_FILE = os.path.join(LOG_DIR, 'jumpserver.log') DRF_EXCEPTION_LOG_FILE = os.path.join(LOG_DIR, 'drf_exception.log') UNEXPECTED_EXCEPTION_LOG_FILE = os.path.join(LOG_DIR, 'unexpected_exception.log') @@ -132,7 +133,6 @@ LOGGING = { 'handlers': ['null'], 'level': 'ERROR' } - } } diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index f005f81f0..8fbea643c 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -1,12 +1,12 @@ # ~*~ coding: utf-8 ~*~ from __future__ import unicode_literals + import os import private_storage.urls - -from django.urls import path, include, re_path from django.conf import settings from django.conf.urls.static import static +from django.urls import path, include, re_path from django.views.i18n import JavaScriptCatalog from . import views, api @@ -49,13 +49,11 @@ if settings.XPACK_ENABLED: urlpatterns = [ path('', views.IndexView.as_view(), name='index'), path('api/v1/', include(api_v1)), - re_path('api/(?P\w+)/(?Pv\d)/.*', views.redirect_format_api), path('api/health/', api.HealthCheckView.as_view(), name="health"), path('api/v1/health/', api.HealthCheckView.as_view(), name="health_v1"), # External apps url path('core/auth/captcha/', include('captcha.urls')), path('core/', include(app_view_patterns)), - path('ui/', views.UIView.as_view()), ] # 静态文件处理路由 @@ -64,6 +62,11 @@ urlpatterns += [ # Protect media path('media/', include(private_storage.urls)), ] +if settings.DEBUG: + urlpatterns += static('/luna/', document_root=(settings.DATA_DIR + '/luna')) + urlpatterns += static('/ui/', document_root=(settings.DATA_DIR + '/lina')) +else: + urlpatterns += path('ui/', views.UIView.as_view()), # js i18n 路由文件 urlpatterns += [ diff --git a/apps/locale/ja/LC_MESSAGES/django.mo b/apps/locale/ja/LC_MESSAGES/django.mo index 3d7539330..4a4ff2f0b 100644 --- a/apps/locale/ja/LC_MESSAGES/django.mo +++ b/apps/locale/ja/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a76aa384867a4732eb7d2365515a1a972502ebadcba4de8236c1dcb3c5c7fdd2 -size 145757 +oid sha256:2d100b8957a5be7b3819b1f47bbd0e0f358c4d77112afb2aa8072f034372a412 +size 148962 diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po index aee6c1f8c..5797de8e5 100644 --- a/apps/locale/ja/LC_MESSAGES/django.po +++ b/apps/locale/ja/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-06-14 20:40+0800\n" +"POT-Creation-Date: 2023-07-20 18:40+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -28,7 +28,7 @@ msgstr "パラメータ 'action' は [{}] でなければなりません。" #: authentication/confirm/password.py:9 authentication/forms.py:32 #: authentication/templates/authentication/login.html:274 #: settings/serializers/auth/ldap.py:25 settings/serializers/auth/ldap.py:47 -#: users/forms/profile.py:22 users/serializers/user.py:105 +#: users/forms/profile.py:22 users/serializers/user.py:104 #: users/templates/users/_msg_user_created.html:13 #: users/templates/users/user_password_verify.html:18 #: xpack/plugins/cloud/serializers/account_attrs.py:28 @@ -49,45 +49,53 @@ msgstr "アクセスキー" msgid "Token" msgstr "トークン" -#: accounts/const/account.py:13 common/db/fields.py:244 +#: accounts/const/account.py:10 +msgid "API key" +msgstr "" + +#: accounts/const/account.py:14 common/db/fields.py:244 #: settings/serializers/terminal.py:14 msgid "All" msgstr "すべて" -#: accounts/const/account.py:14 +#: accounts/const/account.py:15 msgid "Manual input" msgstr "手動入力" -#: accounts/const/account.py:15 +#: accounts/const/account.py:16 msgid "Dynamic user" msgstr "動的コード" -#: accounts/const/account.py:19 users/models/user.py:699 +#: accounts/const/account.py:17 +msgid "Anonymous account" +msgstr "匿名ユーザー" + +#: accounts/const/account.py:21 users/models/user.py:699 msgid "Local" msgstr "ローカル" -#: accounts/const/account.py:20 +#: accounts/const/account.py:22 msgid "Collected" msgstr "集めました" -#: accounts/const/account.py:21 accounts/serializers/account/account.py:27 +#: accounts/const/account.py:23 accounts/serializers/account/account.py:27 #: settings/serializers/auth/sms.py:75 msgid "Template" msgstr "テンプレート" -#: accounts/const/account.py:25 ops/const.py:45 +#: accounts/const/account.py:27 ops/const.py:45 msgid "Skip" msgstr "スキップ" -#: accounts/const/account.py:26 audits/const.py:24 rbac/tree.py:229 +#: accounts/const/account.py:28 audits/const.py:24 rbac/tree.py:230 #: templates/_csv_import_export.html:18 templates/_csv_update_modal.html:6 msgid "Update" msgstr "更新" -#: accounts/const/account.py:27 +#: accounts/const/account.py:29 #: accounts/serializers/automations/change_secret.py:156 audits/const.py:54 #: audits/signal_handlers/activity_log.py:33 common/const/choices.py:19 -#: ops/const.py:58 terminal/const.py:61 xpack/plugins/cloud/const.py:41 +#: ops/const.py:58 terminal/const.py:77 xpack/plugins/cloud/const.py:43 msgid "Failed" msgstr "失敗しました" @@ -186,18 +194,19 @@ msgstr "作成のみ" #: accounts/serializers/account/gathered_account.py:10 #: accounts/serializers/automations/change_secret.py:112 #: accounts/serializers/automations/change_secret.py:132 -#: acls/serializers/base.py:118 assets/models/asset/common.py:93 -#: assets/models/asset/common.py:332 assets/models/cmd_filter.py:36 +#: acls/serializers/base.py:123 assets/models/asset/common.py:93 +#: assets/models/asset/common.py:331 assets/models/cmd_filter.py:36 #: assets/serializers/domain.py:19 assets/serializers/label.py:27 -#: audits/models.py:53 authentication/models/connection_token.py:35 +#: audits/models.py:53 authentication/models/connection_token.py:36 #: perms/models/asset_permission.py:64 perms/serializers/permission.py:34 -#: terminal/backends/command/models.py:20 terminal/models/session/session.py:32 -#: terminal/notifications.py:95 terminal/serializers/command.py:17 +#: terminal/backends/command/models.py:18 terminal/models/session/session.py:31 +#: terminal/notifications.py:155 terminal/serializers/command.py:18 +#: terminal/templates/terminal/_msg_command_warning.html:4 #: tickets/models/ticket/apply_asset.py:16 xpack/plugins/cloud/models.py:212 msgid "Asset" msgstr "資産" -#: accounts/models/account.py:53 accounts/models/account.py:113 +#: accounts/models/account.py:53 accounts/models/account.py:126 #: accounts/serializers/account/account.py:208 #: accounts/serializers/account/account.py:247 #: accounts/serializers/account/template.py:16 @@ -222,10 +231,11 @@ msgstr "ソース ID" #: accounts/models/account.py:61 #: accounts/serializers/automations/change_secret.py:113 #: accounts/serializers/automations/change_secret.py:133 -#: acls/serializers/base.py:119 assets/serializers/asset/common.py:125 +#: acls/serializers/base.py:124 assets/serializers/asset/common.py:125 #: assets/serializers/gateway.py:28 audits/models.py:54 ops/models/base.py:18 #: perms/models/asset_permission.py:70 perms/serializers/permission.py:39 -#: terminal/backends/command/models.py:21 terminal/models/session/session.py:34 +#: terminal/backends/command/models.py:19 terminal/models/session/session.py:33 +#: terminal/templates/terminal/_msg_command_warning.html:8 #: tickets/models/ticket/command_confirm.py:13 xpack/plugins/cloud/models.py:85 msgid "Account" msgstr "アカウント" @@ -250,15 +260,15 @@ msgstr "アカウントを確認できます" msgid "Can push account" msgstr "アカウントをプッシュできます" -#: accounts/models/account.py:117 +#: accounts/models/account.py:130 msgid "Account template" msgstr "アカウント テンプレート" -#: accounts/models/account.py:122 +#: accounts/models/account.py:135 msgid "Can view asset account template secret" msgstr "アセット アカウント テンプレートのパスワードを表示できます" -#: accounts/models/account.py:123 +#: accounts/models/account.py:136 msgid "Can change asset account template secret" msgstr "アセット アカウント テンプレートのパスワードを変更できます" @@ -276,10 +286,10 @@ msgstr "アカウントバックアップ計画" #: accounts/models/automations/backup_account.py:83 #: assets/models/automations/base.py:115 audits/models.py:60 -#: ops/models/base.py:55 ops/models/celery.py:63 ops/models/job.py:192 +#: ops/models/base.py:55 ops/models/celery.py:63 ops/models/job.py:194 #: ops/templates/ops/celery_task_log.html:75 #: perms/models/asset_permission.py:72 terminal/models/applet/host.py:137 -#: terminal/models/session/session.py:45 +#: terminal/models/session/session.py:44 #: tickets/models/ticket/apply_application.py:30 #: tickets/models/ticket/apply_asset.py:19 msgid "Date start" @@ -311,7 +321,7 @@ msgstr "理由" #: accounts/models/automations/backup_account.py:99 #: accounts/serializers/automations/change_secret.py:111 #: accounts/serializers/automations/change_secret.py:134 -#: ops/serializers/job.py:56 terminal/serializers/session.py:46 +#: ops/serializers/job.py:56 terminal/serializers/session.py:43 msgid "Is success" msgstr "成功は" @@ -400,7 +410,7 @@ msgstr "開始日" #: accounts/models/automations/change_secret.py:91 #: assets/models/automations/base.py:116 ops/models/base.py:56 -#: ops/models/celery.py:64 ops/models/job.py:193 +#: ops/models/celery.py:64 ops/models/job.py:195 #: terminal/models/applet/host.py:138 msgid "Date finished" msgstr "終了日" @@ -426,7 +436,7 @@ msgstr "最終ログイン日" #: accounts/models/automations/gather_account.py:17 #: accounts/models/automations/push_account.py:15 accounts/models/base.py:34 -#: acls/serializers/base.py:18 acls/serializers/base.py:49 +#: acls/serializers/base.py:19 acls/serializers/base.py:50 #: assets/models/_user.py:23 audits/models.py:179 authentication/forms.py:25 #: authentication/forms.py:27 authentication/models/temp_token.py:9 #: authentication/templates/authentication/_msg_different_city.html:9 @@ -458,8 +468,8 @@ msgstr "アカウントのコレクション" msgid "Triggers" msgstr "トリガー方式" -#: accounts/models/automations/push_account.py:16 acls/models/base.py:46 -#: acls/serializers/base.py:56 assets/models/cmd_filter.py:81 +#: accounts/models/automations/push_account.py:16 acls/models/base.py:41 +#: acls/serializers/base.py:57 assets/models/cmd_filter.py:81 #: audits/models.py:87 audits/serializers.py:82 #: authentication/serializers/connect_token_secret.py:116 #: authentication/templates/authentication/_access_key_modal.html:34 @@ -474,22 +484,22 @@ msgstr "アカウントプッシュ" msgid "Verify asset account" msgstr "アカウントの確認" -#: accounts/models/base.py:33 acls/models/base.py:40 acls/models/base.py:101 -#: acls/models/command_acl.py:21 acls/serializers/base.py:34 +#: accounts/models/base.py:33 acls/models/base.py:35 acls/models/base.py:96 +#: acls/models/command_acl.py:21 acls/serializers/base.py:35 #: applications/models.py:9 assets/models/_user.py:22 #: assets/models/asset/common.py:91 assets/models/asset/common.py:149 #: assets/models/cmd_filter.py:21 assets/models/domain.py:18 -#: assets/models/group.py:20 assets/models/label.py:18 -#: assets/models/platform.py:13 assets/models/platform.py:81 -#: assets/serializers/asset/common.py:145 assets/serializers/platform.py:99 -#: assets/serializers/platform.py:199 +#: assets/models/group.py:17 assets/models/label.py:18 +#: assets/models/platform.py:15 assets/models/platform.py:88 +#: assets/serializers/asset/common.py:146 assets/serializers/platform.py:110 +#: assets/serializers/platform.py:223 #: authentication/serializers/connect_token_secret.py:110 ops/mixin.py:21 #: ops/models/adhoc.py:21 ops/models/celery.py:15 ops/models/celery.py:57 -#: ops/models/job.py:92 ops/models/playbook.py:23 ops/serializers/job.py:20 +#: ops/models/job.py:94 ops/models/playbook.py:23 ops/serializers/job.py:20 #: orgs/models.py:80 perms/models/asset_permission.py:56 rbac/models/role.py:29 #: settings/models.py:33 settings/serializers/sms.py:6 #: terminal/models/applet/applet.py:32 terminal/models/component/endpoint.py:12 -#: terminal/models/component/endpoint.py:90 +#: terminal/models/component/endpoint.py:92 #: terminal/models/component/storage.py:26 terminal/models/component/task.py:15 #: terminal/models/component/terminal.py:84 users/forms/profile.py:33 #: users/models/group.py:13 users/models/user.py:753 @@ -505,7 +515,8 @@ msgstr "特権アカウント" #: assets/models/automations/base.py:21 assets/models/cmd_filter.py:39 #: assets/models/label.py:22 #: authentication/serializers/connect_token_secret.py:114 -#: terminal/models/applet/applet.py:39 users/serializers/user.py:170 +#: terminal/models/applet/applet.py:39 +#: terminal/models/component/endpoint.py:103 users/serializers/user.py:169 msgid "Is active" msgstr "アクティブです。" @@ -559,9 +570,9 @@ msgid "Exist policy" msgstr "アカウントの存在ポリシー" #: accounts/serializers/account/account.py:180 applications/models.py:11 -#: assets/models/label.py:21 assets/models/platform.py:82 +#: assets/models/label.py:21 assets/models/platform.py:89 #: assets/serializers/asset/common.py:121 assets/serializers/cagegory.py:8 -#: assets/serializers/platform.py:117 assets/serializers/platform.py:200 +#: assets/serializers/platform.py:128 assets/serializers/platform.py:224 #: perms/serializers/user_permission.py:26 settings/models.py:35 #: tickets/models/ticket/apply_application.py:13 msgid "Category" @@ -571,14 +582,14 @@ msgstr "カテゴリ" #: accounts/serializers/automations/base.py:54 acls/models/command_acl.py:24 #: acls/serializers/command_acl.py:18 applications/models.py:14 #: assets/models/_user.py:50 assets/models/automations/base.py:20 -#: assets/models/cmd_filter.py:74 assets/models/platform.py:83 -#: assets/serializers/asset/common.py:122 assets/serializers/platform.py:101 -#: assets/serializers/platform.py:116 audits/serializers.py:48 -#: authentication/serializers/connect_token_secret.py:123 ops/models/job.py:103 +#: assets/models/cmd_filter.py:74 assets/models/platform.py:90 +#: assets/serializers/asset/common.py:122 assets/serializers/platform.py:112 +#: assets/serializers/platform.py:127 audits/serializers.py:48 +#: authentication/serializers/connect_token_secret.py:123 ops/models/job.py:105 #: perms/serializers/user_permission.py:27 terminal/models/applet/applet.py:38 #: terminal/models/component/storage.py:57 #: terminal/models/component/storage.py:146 terminal/serializers/applet.py:29 -#: terminal/serializers/session.py:23 terminal/serializers/storage.py:224 +#: terminal/serializers/session.py:20 terminal/serializers/storage.py:224 #: terminal/serializers/storage.py:236 tickets/models/comment.py:26 #: tickets/models/flow.py:56 tickets/models/ticket/apply_application.py:16 #: tickets/models/ticket/general.py:275 tickets/serializers/flow.py:53 @@ -607,10 +618,10 @@ msgid "Changed" msgstr "編集済み" #: accounts/serializers/account/account.py:250 -#: accounts/serializers/automations/base.py:22 acls/models/base.py:102 +#: accounts/serializers/automations/base.py:22 acls/models/base.py:97 #: assets/models/automations/base.py:19 #: assets/serializers/automations/base.py:20 ops/models/base.py:17 -#: ops/models/job.py:105 ops/serializers/job.py:21 +#: ops/models/job.py:107 ops/serializers/job.py:21 #: terminal/templates/terminal/_msg_command_execute_alert.html:16 msgid "Assets" msgstr "資産" @@ -635,26 +646,27 @@ msgstr "アカウントはすでに存在しています" msgid "ID" msgstr "ID" -#: accounts/serializers/account/account.py:427 acls/serializers/base.py:111 +#: accounts/serializers/account/account.py:427 acls/serializers/base.py:116 #: assets/models/cmd_filter.py:24 assets/models/label.py:16 audits/models.py:49 #: audits/models.py:85 audits/models.py:163 -#: authentication/models/connection_token.py:31 +#: authentication/models/connection_token.py:32 #: authentication/models/sso_token.py:16 #: notifications/models/notification.py:12 #: perms/api/user_permission/mixin.py:55 perms/models/asset_permission.py:58 -#: perms/serializers/permission.py:30 rbac/builtin.py:122 -#: rbac/models/rolebinding.py:49 terminal/backends/command/models.py:19 -#: terminal/models/session/session.py:30 terminal/models/session/sharing.py:32 -#: terminal/notifications.py:96 terminal/notifications.py:144 -#: terminal/serializers/command.py:16 tickets/models/comment.py:21 -#: users/const.py:14 users/models/user.py:947 users/models/user.py:978 -#: users/serializers/group.py:18 +#: perms/serializers/permission.py:30 rbac/builtin.py:123 +#: rbac/models/rolebinding.py:49 terminal/backends/command/models.py:17 +#: terminal/models/session/session.py:29 terminal/models/session/sharing.py:32 +#: terminal/notifications.py:156 terminal/notifications.py:205 +#: terminal/serializers/command.py:17 +#: terminal/templates/terminal/_msg_command_warning.html:6 +#: tickets/models/comment.py:21 users/const.py:14 users/models/user.py:947 +#: users/models/user.py:978 users/serializers/group.py:18 msgid "User" msgstr "ユーザー" #: accounts/serializers/account/account.py:428 #: authentication/templates/authentication/_access_key_modal.html:33 -#: terminal/notifications.py:98 terminal/notifications.py:146 +#: terminal/notifications.py:158 terminal/notifications.py:207 msgid "Date" msgstr "日付" @@ -684,13 +696,17 @@ msgid "Key password" msgstr "キーパスワード" #: accounts/serializers/account/base.py:80 -#: assets/serializers/asset/common.py:306 +#: assets/serializers/asset/common.py:309 msgid "Spec info" msgstr "特別情報" -#: accounts/serializers/account/base.py:81 -msgid "Tip: If no username is required for authentication, fill in `null`" -msgstr "ヒント: 認証にユーザー名が必要ない場合は、null を入力してください" +#: accounts/serializers/account/base.py:82 +msgid "" +"Tip: If no username is required for authentication, fill in `null`, If AD " +"account, like `username@domain`" +msgstr "" +"ヒント: 認証にユーザー名が必要ない場合は、`null`を入力します。ADアカウントの" +"場合は、`username@domain`のようになります。" #: accounts/serializers/automations/base.py:23 #: assets/models/asset/common.py:155 assets/models/automations/base.py:18 @@ -729,7 +745,7 @@ msgstr "自動タスク実行履歴" #: accounts/serializers/automations/change_secret.py:155 audits/const.py:53 #: audits/models.py:59 audits/signal_handlers/activity_log.py:33 #: common/const/choices.py:18 ops/const.py:56 ops/serializers/celery.py:40 -#: terminal/const.py:60 terminal/models/session/sharing.py:107 +#: terminal/const.py:76 terminal/models/session/sharing.py:107 #: tickets/views/approve.py:114 msgid "Success" msgstr "成功" @@ -778,57 +794,62 @@ msgstr "秘密鍵が無効またはpassphraseエラー" msgid "Acls" msgstr "Acls" -#: acls/models/base.py:20 tickets/const.py:45 +#: acls/const.py:6 terminal/const.py:11 tickets/const.py:45 #: tickets/templates/tickets/approve_check_password.html:49 msgid "Reject" msgstr "拒否" -#: acls/models/base.py:21 +#: acls/const.py:7 terminal/const.py:9 msgid "Accept" msgstr "受け入れられる" -#: acls/models/base.py:22 +#: acls/const.py:8 msgid "Review" msgstr "レビュー担当者" -#: acls/models/base.py:42 assets/models/_user.py:51 -#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:93 +#: acls/const.py:9 terminal/const.py:10 +msgid "Warning" +msgstr "警告" + +#: acls/models/base.py:37 assets/models/_user.py:51 +#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:95 msgid "Priority" msgstr "優先順位" -#: acls/models/base.py:43 assets/models/_user.py:51 -#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:94 +#: acls/models/base.py:38 assets/models/_user.py:51 +#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:96 msgid "1-100, the lower the value will be match first" msgstr "1-100、低い値は最初に一致します" -#: acls/models/base.py:47 assets/models/cmd_filter.py:86 +#: acls/models/base.py:42 assets/models/cmd_filter.py:86 #: authentication/serializers/connect_token_secret.py:88 msgid "Reviewers" msgstr "レビュー担当者" -#: acls/models/base.py:48 authentication/models/access_key.py:17 -#: authentication/models/connection_token.py:52 +#: acls/models/base.py:43 authentication/models/access_key.py:17 +#: authentication/models/connection_token.py:53 #: authentication/templates/authentication/_access_key_modal.html:32 #: perms/models/asset_permission.py:76 terminal/models/session/sharing.py:27 #: tickets/const.py:37 msgid "Active" msgstr "アクティブ" -#: acls/models/base.py:86 users/apps.py:9 +#: acls/models/base.py:81 users/apps.py:9 msgid "Users" msgstr "ユーザー" -#: acls/models/base.py:103 assets/models/automations/base.py:17 -#: assets/models/cmd_filter.py:38 assets/serializers/asset/common.py:305 +#: acls/models/base.py:98 assets/models/automations/base.py:17 +#: assets/models/cmd_filter.py:38 assets/serializers/asset/common.py:308 #: rbac/tree.py:35 msgid "Accounts" msgstr "アカウント" #: acls/models/command_acl.py:16 assets/models/cmd_filter.py:60 -#: ops/serializers/job.py:55 terminal/const.py:68 -#: terminal/models/session/session.py:43 terminal/serializers/command.py:18 +#: ops/serializers/job.py:55 terminal/const.py:84 +#: terminal/models/session/session.py:42 terminal/serializers/command.py:19 #: terminal/templates/terminal/_msg_command_alert.html:12 #: terminal/templates/terminal/_msg_command_execute_alert.html:10 +#: terminal/templates/terminal/_msg_command_warning.html:23 msgid "Command" msgstr "コマンド" @@ -852,6 +873,7 @@ msgstr "家を無視する" #: acls/models/command_acl.py:33 acls/models/command_acl.py:96 #: acls/serializers/command_acl.py:28 #: authentication/serializers/connect_token_secret.py:85 +#: terminal/templates/terminal/_msg_command_warning.html:14 msgid "Command group" msgstr "コマンドグループ" @@ -860,6 +882,7 @@ msgid "The generated regular expression is incorrect: {}" msgstr "生成された正規表現が正しくありません: {}" #: acls/models/command_acl.py:100 +#: terminal/templates/terminal/_msg_command_warning.html:12 msgid "Command acl" msgstr "コマンドフィルタリング" @@ -896,11 +919,11 @@ msgstr "ログインasset acl" msgid "Login asset confirm" msgstr "ログイン資産の確認" -#: acls/serializers/base.py:10 acls/serializers/login_acl.py:11 +#: acls/serializers/base.py:11 acls/serializers/login_acl.py:11 msgid "With * indicating a match all. " msgstr "* はすべて一致することを示します。" -#: acls/serializers/base.py:25 +#: acls/serializers/base.py:26 msgid "" "With * indicating a match all. Such as: 192.168.10.1, 192.168.1.0/24, " "10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 (Domain name " @@ -910,15 +933,19 @@ msgstr "" "10.1.1.1-10.1.1.20、2001:db8:2de::e13、2001:db8:1a:1110:::/64 (ドメイン名サ" "ポート)" -#: acls/serializers/base.py:40 assets/serializers/asset/host.py:19 +#: acls/serializers/base.py:41 assets/serializers/asset/host.py:19 msgid "IP/Host" msgstr "IP/ホスト" -#: acls/serializers/base.py:98 tickets/serializers/ticket/ticket.py:77 +#: acls/serializers/base.py:91 +msgid "Recipients" +msgstr "受信者" + +#: acls/serializers/base.py:103 tickets/serializers/ticket/ticket.py:77 msgid "The organization `{}` does not exist" msgstr "組織 '{}'は存在しません" -#: acls/serializers/base.py:104 +#: acls/serializers/base.py:109 msgid "None of the reviewers belong to Organization `{}`" msgstr "いずれのレビューアも組織 '{}' に属していません" @@ -952,7 +979,7 @@ msgid "Applications" msgstr "アプリケーション" #: applications/models.py:16 xpack/plugins/cloud/models.py:33 -#: xpack/plugins/cloud/serializers/account.py:62 +#: xpack/plugins/cloud/serializers/account.py:63 msgid "Attrs" msgstr "ツールバーの" @@ -964,13 +991,13 @@ msgstr "アプリケーション" msgid "Can match application" msgstr "アプリケーションを一致させることができます" -#: assets/api/asset/asset.py:149 +#: assets/api/asset/asset.py:157 msgid "Cannot create asset directly, you should create a host or other" msgstr "" "資産を直接作成することはできません。ホストまたはその他を作成する必要がありま" "す" -#: assets/api/domain.py:60 +#: assets/api/domain.py:62 msgid "Number required" msgstr "必要な数" @@ -999,7 +1026,7 @@ msgid "{} disabled" msgstr "{} 無効" #: assets/automations/ping_gateway/manager.py:33 -#: authentication/models/connection_token.py:117 +#: authentication/models/connection_token.py:128 msgid "No account" msgstr "アカウントなし" @@ -1044,6 +1071,20 @@ msgstr "テストゲートウェイ" msgid "Gather facts" msgstr "資産情報の収集" +#: assets/const/base.py:33 audits/const.py:47 +#: terminal/serializers/applet_host.py:32 +msgid "Disabled" +msgstr "無効" + +#: assets/const/base.py:34 settings/serializers/basic.py:27 +msgid "Basic" +msgstr "基本" + +#: assets/const/base.py:35 assets/const/protocol.py:193 +#: assets/models/asset/web.py:13 +msgid "Script" +msgstr "脚本" + #: assets/const/category.py:10 assets/models/asset/host.py:8 #: settings/serializers/auth/radius.py:16 settings/serializers/auth/sms.py:67 #: terminal/models/component/endpoint.py:13 terminal/serializers/applet.py:17 @@ -1064,12 +1105,13 @@ msgstr "データベース" msgid "Cloud service" msgstr "クラウド サービス" -#: assets/const/category.py:14 assets/models/asset/web.py:16 audits/const.py:34 +#: assets/const/category.py:14 assets/models/asset/gpt.py:11 +#: assets/models/asset/web.py:16 audits/const.py:34 #: terminal/models/applet/applet.py:26 msgid "Web" msgstr "Web" -#: assets/const/category.py:15 common/sdk/sms/endpoint.py:20 +#: assets/const/category.py:16 common/sdk/sms/endpoint.py:20 msgid "Custom type" msgstr "カスタムタイプ" @@ -1102,32 +1144,84 @@ msgstr "ルーター" msgid "Firewall" msgstr "ファイアウォール" +#: assets/const/gpt.py:7 +msgid "ChatGPT" +msgstr "" + #: assets/const/host.py:12 rbac/tree.py:28 msgid "Other" msgstr "その他" -#: assets/const/types.py:223 +#: assets/const/protocol.py:43 +msgid "SFTP enabled" +msgstr "SFTP が有効" + +#: assets/const/protocol.py:48 +msgid "SFTP home" +msgstr "SFTP ルート パス" + +#: assets/const/protocol.py:59 +msgid "Console" +msgstr "Console" + +#: assets/const/protocol.py:60 +msgid "Connect to console session" +msgstr "コンソールセッションに接続" + +#: assets/const/protocol.py:64 +msgid "Any" +msgstr "任意" + +#: assets/const/protocol.py:66 settings/serializers/security.py:151 +msgid "Security" +msgstr "セキュリティ" + +#: assets/const/protocol.py:67 +msgid "Security layer to use for the connection" +msgstr "接続に使用するセキュリティ レイヤー" + +#: assets/const/protocol.py:73 +msgid "AD domain" +msgstr "AD ドメイン" + +#: assets/const/protocol.py:92 assets/models/asset/database.py:10 +#: settings/serializers/email.py:37 +msgid "Use SSL" +msgstr "SSLの使用" + +#: assets/const/protocol.py:149 +msgid "Auth username" +msgstr "ユーザー名で認証する" + +#: assets/const/protocol.py:170 assets/models/asset/web.py:9 +#: assets/serializers/asset/info/spec.py:16 +msgid "Autofill" +msgstr "自動充填" + +#: assets/const/protocol.py:178 assets/models/asset/web.py:10 +msgid "Username selector" +msgstr "ユーザー名ピッカー" + +#: assets/const/protocol.py:183 assets/models/asset/web.py:11 +msgid "Password selector" +msgstr "パスワードセレクター" + +#: assets/const/protocol.py:188 assets/models/asset/web.py:12 +msgid "Submit selector" +msgstr "ボタンセレクターを確認する" + +#: assets/const/protocol.py:211 +msgid "API mode" +msgstr "APIモード" + +#: assets/const/types.py:224 msgid "All types" msgstr "いろんなタイプ" -#: assets/const/web.py:8 +#: assets/const/web.py:7 msgid "Website" msgstr "Webサイト" -#: assets/const/web.py:59 audits/const.py:47 -#: terminal/serializers/applet_host.py:32 -msgid "Disabled" -msgstr "無効" - -#: assets/const/web.py:60 settings/serializers/basic.py:27 -msgid "Basic" -msgstr "基本" - -#: assets/const/web.py:61 assets/models/asset/web.py:13 -#: assets/serializers/platform.py:40 -msgid "Script" -msgstr "脚本" - #: assets/exceptions.py:12 msgid "This function is not supported temporarily" msgstr "この機能は一時的にサポートされていません" @@ -1141,21 +1235,21 @@ msgid "SSH public key" msgstr "SSHパブリックキー" #: assets/models/_user.py:27 assets/models/cmd_filter.py:40 -#: assets/models/cmd_filter.py:88 assets/models/group.py:23 -#: common/db/models.py:36 ops/models/adhoc.py:27 ops/models/job.py:111 +#: assets/models/cmd_filter.py:88 assets/models/group.py:20 +#: common/db/models.py:36 ops/models/adhoc.py:27 ops/models/job.py:113 #: ops/models/playbook.py:26 rbac/models/role.py:37 settings/models.py:38 -#: terminal/models/applet/applet.py:44 terminal/models/applet/applet.py:244 +#: terminal/models/applet/applet.py:44 terminal/models/applet/applet.py:248 #: terminal/models/applet/host.py:139 terminal/models/component/endpoint.py:24 -#: terminal/models/component/endpoint.py:100 -#: terminal/models/session/session.py:47 tickets/models/comment.py:32 +#: terminal/models/component/endpoint.py:102 +#: terminal/models/session/session.py:46 tickets/models/comment.py:32 #: tickets/models/ticket/general.py:297 users/models/user.py:792 #: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:111 msgid "Comment" msgstr "コメント" #: assets/models/_user.py:28 assets/models/automations/base.py:114 -#: assets/models/cmd_filter.py:41 assets/models/group.py:22 -#: common/db/models.py:34 ops/models/base.py:54 ops/models/job.py:191 +#: assets/models/cmd_filter.py:41 assets/models/group.py:19 +#: common/db/models.py:34 ops/models/base.py:54 ops/models/job.py:193 #: users/models/user.py:979 msgid "Date created" msgstr "作成された日付" @@ -1166,7 +1260,7 @@ msgid "Date updated" msgstr "更新日" #: assets/models/_user.py:30 assets/models/cmd_filter.py:44 -#: assets/models/cmd_filter.py:91 assets/models/group.py:21 +#: assets/models/cmd_filter.py:91 assets/models/group.py:18 #: common/db/models.py:32 users/models/user.py:799 #: users/serializers/group.py:29 msgid "Created by" @@ -1192,10 +1286,10 @@ msgstr "管理ユーザー" msgid "Username same with user" msgstr "ユーザーと同じユーザー名" -#: assets/models/_user.py:52 authentication/models/connection_token.py:40 +#: assets/models/_user.py:52 authentication/models/connection_token.py:41 #: authentication/serializers/connect_token_secret.py:111 -#: terminal/models/applet/applet.py:41 terminal/serializers/session.py:21 -#: terminal/serializers/session.py:42 terminal/serializers/storage.py:68 +#: terminal/models/applet/applet.py:41 terminal/serializers/session.py:18 +#: terminal/serializers/session.py:39 terminal/serializers/storage.py:68 msgid "Protocol" msgstr "プロトコル" @@ -1247,17 +1341,17 @@ msgstr "システムユーザーに一致できます" msgid "Cloud" msgstr "クラウド サービス" -#: assets/models/asset/common.py:92 assets/models/platform.py:14 +#: assets/models/asset/common.py:92 assets/models/platform.py:16 #: settings/serializers/auth/radius.py:17 settings/serializers/auth/sms.py:68 #: xpack/plugins/cloud/serializers/account_attrs.py:73 msgid "Port" msgstr "ポート" -#: assets/models/asset/common.py:150 assets/serializers/asset/common.py:146 +#: assets/models/asset/common.py:150 assets/serializers/asset/common.py:147 msgid "Address" msgstr "アドレス" -#: assets/models/asset/common.py:151 assets/models/platform.py:112 +#: assets/models/asset/common.py:151 assets/models/platform.py:119 #: authentication/serializers/connect_token_secret.py:115 #: perms/serializers/user_permission.py:24 #: xpack/plugins/cloud/serializers/account_attrs.py:196 @@ -1274,28 +1368,28 @@ msgstr "ドメイン" msgid "Labels" msgstr "ラベル" -#: assets/models/asset/common.py:158 assets/serializers/asset/common.py:307 +#: assets/models/asset/common.py:158 assets/serializers/asset/common.py:310 #: assets/serializers/asset/host.py:11 msgid "Gathered info" msgstr "資産ハードウェア情報の収集" #: assets/models/asset/common.py:159 assets/serializers/asset/custom.py:14 msgid "Custom info" -msgstr "自动化信息" +msgstr "カスタム属性" -#: assets/models/asset/common.py:335 +#: assets/models/asset/common.py:334 msgid "Can refresh asset hardware info" msgstr "資産ハードウェア情報を更新できます" -#: assets/models/asset/common.py:336 +#: assets/models/asset/common.py:335 msgid "Can test asset connectivity" msgstr "資産接続をテストできます" -#: assets/models/asset/common.py:337 +#: assets/models/asset/common.py:336 msgid "Can match asset" msgstr "アセットを一致させることができます" -#: assets/models/asset/common.py:338 +#: assets/models/asset/common.py:337 msgid "Can change asset nodes" msgstr "資産ノードを変更できます" @@ -1303,11 +1397,6 @@ msgstr "資産ノードを変更できます" msgid "Custom asset" msgstr "カスタム アセット" -#: assets/models/asset/database.py:10 assets/serializers/platform.py:45 -#: settings/serializers/email.py:37 -msgid "Use SSL" -msgstr "SSLの使用" - #: assets/models/asset/database.py:11 msgid "CA cert" msgstr "CA 証明書" @@ -1324,24 +1413,11 @@ msgstr "クライアントキー" msgid "Allow invalid cert" msgstr "証明書チェックを無視" -#: assets/models/asset/web.py:9 assets/serializers/asset/info/spec.py:16 -#: assets/serializers/platform.py:30 -msgid "Autofill" -msgstr "自動充填" +#: assets/models/asset/gpt.py:8 +msgid "Proxy" +msgstr "プロキシー" -#: assets/models/asset/web.py:10 assets/serializers/platform.py:32 -msgid "Username selector" -msgstr "ユーザー名ピッカー" - -#: assets/models/asset/web.py:11 assets/serializers/platform.py:35 -msgid "Password selector" -msgstr "パスワードセレクター" - -#: assets/models/asset/web.py:12 assets/serializers/platform.py:38 -msgid "Submit selector" -msgstr "ボタンセレクターを確認する" - -#: assets/models/automations/base.py:22 ops/models/job.py:187 +#: assets/models/automations/base.py:22 ops/models/job.py:189 #: settings/serializers/auth/sms.py:99 msgid "Parameters" msgstr "パラメータ" @@ -1355,8 +1431,8 @@ msgid "Asset automation task" msgstr "アセットの自動化タスク" #: assets/models/automations/base.py:113 audits/models.py:199 -#: audits/serializers.py:49 ops/models/base.py:49 ops/models/job.py:184 -#: terminal/models/applet/applet.py:243 terminal/models/applet/host.py:136 +#: audits/serializers.py:49 ops/models/base.py:49 ops/models/job.py:186 +#: terminal/models/applet/applet.py:247 terminal/models/applet/host.py:136 #: terminal/models/component/status.py:30 terminal/serializers/applet.py:18 #: terminal/serializers/applet_host.py:107 tickets/models/ticket/general.py:283 #: tickets/serializers/super_ticket.py:13 @@ -1419,17 +1495,17 @@ msgstr "お気に入りのアセット" msgid "Gateway" msgstr "ゲートウェイ" -#: assets/models/group.py:30 +#: assets/models/group.py:27 msgid "Asset group" msgstr "資産グループ" -#: assets/models/group.py:34 assets/models/platform.py:17 -#: assets/serializers/platform.py:102 +#: assets/models/group.py:31 assets/models/platform.py:19 +#: assets/serializers/platform.py:113 #: xpack/plugins/cloud/providers/nutanix.py:30 msgid "Default" msgstr "デフォルト" -#: assets/models/group.py:34 +#: assets/models/group.py:31 msgid "Default asset group" msgstr "デフォルトアセットグループ" @@ -1437,9 +1513,9 @@ msgstr "デフォルトアセットグループ" msgid "System" msgstr "システム" -#: assets/models/label.py:19 assets/models/node.py:557 +#: assets/models/label.py:19 assets/models/node.py:545 #: assets/serializers/cagegory.py:7 assets/serializers/cagegory.py:14 -#: authentication/models/connection_token.py:28 +#: authentication/models/connection_token.py:29 #: authentication/serializers/connect_token_secret.py:122 #: common/serializers/common.py:86 settings/models.py:34 msgid "Value" @@ -1447,7 +1523,7 @@ msgstr "値" #: assets/models/label.py:40 assets/serializers/asset/common.py:123 #: assets/serializers/cagegory.py:6 assets/serializers/cagegory.py:13 -#: assets/serializers/platform.py:100 +#: assets/serializers/platform.py:111 #: authentication/serializers/connect_token_secret.py:121 #: common/serializers/common.py:85 perms/serializers/user_permission.py:28 #: settings/serializers/sms.py:7 @@ -1458,144 +1534,145 @@ msgstr "ラベル" msgid "New node" msgstr "新しいノード" -#: assets/models/node.py:485 audits/backends/db.py:55 audits/backends/db.py:56 +#: assets/models/node.py:473 audits/backends/db.py:55 audits/backends/db.py:56 msgid "empty" msgstr "空" -#: assets/models/node.py:556 perms/models/perm_node.py:28 +#: assets/models/node.py:544 perms/models/perm_node.py:28 msgid "Key" msgstr "キー" -#: assets/models/node.py:558 assets/serializers/node.py:20 +#: assets/models/node.py:546 assets/serializers/node.py:20 msgid "Full value" msgstr "フルバリュー" -#: assets/models/node.py:562 perms/models/perm_node.py:30 +#: assets/models/node.py:550 perms/models/perm_node.py:30 msgid "Parent key" msgstr "親キー" -#: assets/models/node.py:571 perms/serializers/permission.py:35 +#: assets/models/node.py:559 perms/serializers/permission.py:35 #: tickets/models/ticket/apply_asset.py:14 xpack/plugins/cloud/models.py:96 msgid "Node" msgstr "ノード" -#: assets/models/node.py:574 +#: assets/models/node.py:562 msgid "Can match node" msgstr "ノードを一致させることができます" -#: assets/models/platform.py:15 +#: assets/models/platform.py:17 msgid "Primary" msgstr "主要" -#: assets/models/platform.py:16 +#: assets/models/platform.py:18 msgid "Required" msgstr "必要" -#: assets/models/platform.py:18 +#: assets/models/platform.py:20 msgid "Public" msgstr "開ける" -#: assets/models/platform.py:19 settings/serializers/settings.py:67 +#: assets/models/platform.py:21 assets/serializers/platform.py:48 +#: settings/serializers/settings.py:67 #: users/templates/users/reset_password.html:29 msgid "Setting" msgstr "設定" -#: assets/models/platform.py:31 audits/const.py:48 settings/models.py:37 +#: assets/models/platform.py:38 audits/const.py:48 settings/models.py:37 #: terminal/serializers/applet_host.py:33 msgid "Enabled" msgstr "有効化" -#: assets/models/platform.py:32 +#: assets/models/platform.py:39 msgid "Ansible config" msgstr "Ansible 構成" -#: assets/models/platform.py:34 assets/serializers/platform.py:63 +#: assets/models/platform.py:41 assets/serializers/platform.py:32 msgid "Ping enabled" msgstr "アセット ディスカバリを有効にする" -#: assets/models/platform.py:35 assets/serializers/platform.py:64 +#: assets/models/platform.py:42 assets/serializers/platform.py:33 msgid "Ping method" msgstr "資産検出方法" -#: assets/models/platform.py:36 +#: assets/models/platform.py:43 msgid "Ping params" msgstr "資産検出パラメータ" -#: assets/models/platform.py:38 assets/models/platform.py:62 -#: assets/serializers/platform.py:65 +#: assets/models/platform.py:45 assets/models/platform.py:69 +#: assets/serializers/platform.py:34 msgid "Gather facts enabled" msgstr "資産情報の収集を有効にする" -#: assets/models/platform.py:40 assets/models/platform.py:64 -#: assets/serializers/platform.py:66 +#: assets/models/platform.py:47 assets/models/platform.py:71 +#: assets/serializers/platform.py:35 msgid "Gather facts method" msgstr "情報収集の方法" -#: assets/models/platform.py:42 assets/models/platform.py:66 +#: assets/models/platform.py:49 assets/models/platform.py:73 msgid "Gather facts params" msgstr "情報収集パラメータ" -#: assets/models/platform.py:44 assets/serializers/platform.py:69 +#: assets/models/platform.py:51 assets/serializers/platform.py:38 msgid "Change secret enabled" msgstr "パスワードの変更が有効" -#: assets/models/platform.py:46 assets/serializers/platform.py:70 +#: assets/models/platform.py:53 assets/serializers/platform.py:39 msgid "Change secret method" msgstr "パスワード変更モード" -#: assets/models/platform.py:48 +#: assets/models/platform.py:55 msgid "Change secret params" msgstr "パスワード変更パラメータ" -#: assets/models/platform.py:50 assets/serializers/platform.py:71 +#: assets/models/platform.py:57 assets/serializers/platform.py:40 msgid "Push account enabled" msgstr "アカウントのプッシュを有効にする" -#: assets/models/platform.py:52 assets/serializers/platform.py:72 +#: assets/models/platform.py:59 assets/serializers/platform.py:41 msgid "Push account method" msgstr "アカウントプッシュ方式" -#: assets/models/platform.py:54 +#: assets/models/platform.py:61 msgid "Push account params" msgstr "アカウントプッシュパラメータ" -#: assets/models/platform.py:56 assets/serializers/platform.py:67 +#: assets/models/platform.py:63 assets/serializers/platform.py:36 msgid "Verify account enabled" msgstr "アカウントの確認をオンにする" -#: assets/models/platform.py:58 assets/serializers/platform.py:68 +#: assets/models/platform.py:65 assets/serializers/platform.py:37 msgid "Verify account method" msgstr "アカウント認証方法" -#: assets/models/platform.py:60 +#: assets/models/platform.py:67 msgid "Verify account params" msgstr "アカウント認証パラメータ" -#: assets/models/platform.py:84 tickets/models/ticket/general.py:300 +#: assets/models/platform.py:91 tickets/models/ticket/general.py:300 msgid "Meta" msgstr "メタ" -#: assets/models/platform.py:85 +#: assets/models/platform.py:92 msgid "Internal" msgstr "ビルトイン" -#: assets/models/platform.py:89 assets/serializers/platform.py:115 +#: assets/models/platform.py:96 assets/serializers/platform.py:126 msgid "Charset" msgstr "シャーセット" -#: assets/models/platform.py:91 assets/serializers/platform.py:143 +#: assets/models/platform.py:98 assets/serializers/platform.py:154 msgid "Domain enabled" msgstr "ドメインを有効にする" -#: assets/models/platform.py:93 assets/serializers/platform.py:142 +#: assets/models/platform.py:100 assets/serializers/platform.py:153 msgid "Su enabled" msgstr "アカウントの切り替えを有効にする" -#: assets/models/platform.py:94 assets/serializers/platform.py:121 +#: assets/models/platform.py:101 assets/serializers/platform.py:132 msgid "Su method" msgstr "アカウントの切り替え方法" -#: assets/models/platform.py:95 assets/serializers/platform.py:124 +#: assets/models/platform.py:102 assets/serializers/platform.py:135 msgid "Custom fields" msgstr "カスタムフィールド" @@ -1612,7 +1689,7 @@ msgstr "" "プラットフォームタイプがスキップされた資産に合致しない、資産内の一括更新プ" "ラットフォーム" -#: assets/serializers/asset/common.py:124 assets/serializers/platform.py:118 +#: assets/serializers/asset/common.py:124 assets/serializers/platform.py:129 #: authentication/serializers/connect_token_secret.py:29 #: authentication/serializers/connect_token_secret.py:72 #: perms/serializers/user_permission.py:25 xpack/plugins/cloud/models.py:99 @@ -1620,24 +1697,24 @@ msgid "Protocols" msgstr "プロトコル" #: assets/serializers/asset/common.py:126 -#: assets/serializers/asset/common.py:147 +#: assets/serializers/asset/common.py:148 msgid "Node path" msgstr "ノードパスです" -#: assets/serializers/asset/common.py:144 -#: assets/serializers/asset/common.py:308 +#: assets/serializers/asset/common.py:145 +#: assets/serializers/asset/common.py:311 msgid "Auto info" msgstr "自動情報" -#: assets/serializers/asset/common.py:227 +#: assets/serializers/asset/common.py:234 msgid "Platform not exist" msgstr "プラットフォームが存在しません" -#: assets/serializers/asset/common.py:263 +#: assets/serializers/asset/common.py:270 msgid "port out of range (0-65535)" msgstr "ポート番号が範囲外です (0-65535)" -#: assets/serializers/asset/common.py:270 +#: assets/serializers/asset/common.py:277 msgid "Protocol is required: {}" msgstr "プロトコルが必要です: {}" @@ -1645,8 +1722,8 @@ msgstr "プロトコルが必要です: {}" msgid "Default database" msgstr "デフォルト・データベース" -#: assets/serializers/asset/database.py:28 common/db/fields.py:570 -#: common/db/fields.py:575 common/serializers/fields.py:104 +#: assets/serializers/asset/database.py:28 common/db/fields.py:579 +#: common/db/fields.py:584 common/serializers/fields.py:104 #: tickets/serializers/ticket/common.py:58 #: xpack/plugins/cloud/serializers/account_attrs.py:56 #: xpack/plugins/cloud/serializers/account_attrs.py:79 @@ -1654,6 +1731,22 @@ msgstr "デフォルト・データベース" msgid "This field is required." msgstr "このフィールドは必須です。" +#: assets/serializers/asset/gpt.py:20 +msgid "" +"If the server cannot directly connect to the API address, you need set up an " +"HTTP proxy. e.g. http(s)://host:port" +msgstr "" +"サーバーが API アドレスに直接接続できない場合は、HTTP プロキシを設定する必要" +"があります。例: http(s)://host:port" + +#: assets/serializers/asset/gpt.py:24 +msgid "HTTP proxy" +msgstr "HTTP プロキシ" + +#: assets/serializers/asset/gpt.py:31 +msgid "Proxy must start with http:// or https://" +msgstr "エージェントはhttp://またはhttps://で始まる必要があります" + #: assets/serializers/asset/info/gathered.py:6 msgid "Vendor" msgstr "ベンダー" @@ -1728,47 +1821,64 @@ msgstr "値" msgid "Can't contains: /" msgstr "含まれない:/" -#: assets/serializers/platform.py:26 -msgid "SFTP enabled" -msgstr "SFTP が有効" - -#: assets/serializers/platform.py:27 -msgid "SFTP home" -msgstr "SFTP ルート パス" - #: assets/serializers/platform.py:42 -msgid "Auth with username" -msgstr "ユーザー名で認証する" - -#: assets/serializers/platform.py:73 msgid "Gather accounts enabled" msgstr "アカウント収集を有効にする" -#: assets/serializers/platform.py:74 +#: assets/serializers/platform.py:43 msgid "Gather accounts method" msgstr "アカウントの収集方法" -#: assets/serializers/platform.py:103 +#: assets/serializers/platform.py:49 +msgid "Port from addr" +msgstr "アドレスからのポート" + +#: assets/serializers/platform.py:61 +msgid "" +"This protocol is primary, and it must be set when adding assets. " +"Additionally, there can only be one primary protocol." +msgstr "" +"このプロトコルはプライマリであり、資産を追加するときに設定する必要がありま" +"す。また、プライマリプロトコルは1つしかありません" + +#: assets/serializers/platform.py:66 +msgid "This protocol is required, and it must be set when adding assets." +msgstr "このプロトコルは必須であり、資産を追加するときに設定する必要があります" + +#: assets/serializers/platform.py:69 +msgid "" +"This protocol is default, when adding assets, it will be displayed by " +"default." +msgstr "" +"このプロトコルはデフォルトです。資産を追加するときに、デフォルトで表示されま" +"す" + +#: assets/serializers/platform.py:72 +msgid "This protocol is public, asset will show this protocol to user" +msgstr "" +"このプロトコルは公開されており、資産はこのプロトコルをユーザーに表示します" + +#: assets/serializers/platform.py:114 msgid "Help text" msgstr "ヘルプ" -#: assets/serializers/platform.py:104 +#: assets/serializers/platform.py:115 msgid "Choices" msgstr "せんたく" -#: assets/serializers/platform.py:119 +#: assets/serializers/platform.py:130 msgid "Automation" msgstr "オートメーション" -#: assets/serializers/platform.py:144 +#: assets/serializers/platform.py:155 msgid "Default Domain" msgstr "デフォルト ドメイン" -#: assets/serializers/platform.py:153 +#: assets/serializers/platform.py:176 msgid "type is required" msgstr "タイプ このフィールドは必須です." -#: assets/serializers/platform.py:176 +#: assets/serializers/platform.py:199 msgid "Protocols is required" msgstr "同意が必要です" @@ -1862,7 +1972,7 @@ msgstr "Rmdir" #: audits/const.py:14 audits/const.py:25 #: authentication/templates/authentication/_access_key_modal.html:65 -#: perms/const.py:17 rbac/tree.py:230 +#: perms/const.py:17 rbac/tree.py:231 msgid "Delete" msgstr "削除" @@ -1886,13 +1996,14 @@ msgstr "ダウンロード" msgid "Rename dir" msgstr "マップディレクトリ" -#: audits/const.py:23 rbac/tree.py:228 +#: audits/const.py:23 rbac/tree.py:229 +#: terminal/templates/terminal/_msg_command_warning.html:18 msgid "View" msgstr "表示" #: audits/const.py:26 #: authentication/templates/authentication/_access_key_modal.html:22 -#: rbac/tree.py:227 +#: rbac/tree.py:228 msgid "Create" msgstr "作成" @@ -1912,7 +2023,7 @@ msgstr "パスワードを変更する" #: audits/const.py:35 settings/serializers/terminal.py:6 #: terminal/models/applet/host.py:25 terminal/models/component/terminal.py:163 -#: terminal/serializers/session.py:49 terminal/serializers/session.py:58 +#: terminal/serializers/session.py:46 terminal/serializers/session.py:55 msgid "Terminal" msgstr "ターミナル" @@ -1950,7 +2061,7 @@ msgid "Job audit log" msgstr "ジョブ監査ログ" #: audits/models.py:51 audits/models.py:95 audits/models.py:166 -#: terminal/models/session/session.py:39 terminal/models/session/sharing.py:99 +#: terminal/models/session/session.py:38 terminal/models/session/sharing.py:99 msgid "Remote addr" msgstr "リモートaddr" @@ -1966,10 +2077,11 @@ msgstr "ファイル名" msgid "File" msgstr "書類" -#: audits/models.py:62 terminal/backends/command/models.py:24 +#: audits/models.py:62 terminal/backends/command/models.py:22 #: terminal/models/session/replay.py:9 terminal/models/session/sharing.py:18 #: terminal/models/session/sharing.py:81 #: terminal/templates/terminal/_msg_command_alert.html:10 +#: terminal/templates/terminal/_msg_command_warning.html:17 #: tickets/models/ticket/command_confirm.py:15 msgid "Session" msgstr "セッション" @@ -1988,7 +2100,7 @@ msgid "Resource" msgstr "リソース" #: audits/models.py:96 audits/models.py:142 audits/models.py:168 -#: terminal/serializers/command.py:50 +#: terminal/serializers/command.py:76 msgid "Datetime" msgstr "時間" @@ -2090,14 +2202,14 @@ msgid "Auth Token" msgstr "認証トークン" #: audits/signal_handlers/login_log.py:31 authentication/notifications.py:73 -#: authentication/views/login.py:74 authentication/views/wecom.py:159 +#: authentication/views/login.py:75 authentication/views/wecom.py:159 #: notifications/backends/__init__.py:11 settings/serializers/auth/wecom.py:10 #: users/models/user.py:706 users/models/user.py:814 msgid "WeCom" msgstr "企業微信" #: audits/signal_handlers/login_log.py:32 authentication/views/feishu.py:123 -#: authentication/views/login.py:86 notifications/backends/__init__.py:14 +#: authentication/views/login.py:87 notifications/backends/__init__.py:14 #: settings/serializers/auth/feishu.py:10 #: settings/serializers/auth/feishu.py:13 users/models/user.py:708 #: users/models/user.py:816 @@ -2105,7 +2217,7 @@ msgid "FeiShu" msgstr "本を飛ばす" #: audits/signal_handlers/login_log.py:33 authentication/views/dingtalk.py:160 -#: authentication/views/login.py:80 notifications/backends/__init__.py:12 +#: authentication/views/login.py:81 notifications/backends/__init__.py:12 #: settings/serializers/auth/dingtalk.py:10 users/models/user.py:707 #: users/models/user.py:815 msgid "DingTalk" @@ -2128,19 +2240,29 @@ msgstr "外部ストレージへのFTPファイルのアップロード" msgid "This action require verify your MFA" msgstr "この操作には、MFAを検証する必要があります" -#: authentication/api/connection_token.py:305 +#: authentication/api/connection_token.py:221 +msgid "Reusable connection token is not allowed, global setting not enabled" +msgstr "" +"再使用可能な接続トークンの使用は許可されていません。グローバル設定は有効に" +"なっていません" + +#: authentication/api/connection_token.py:301 +msgid "Anonymous account is not supported for this asset" +msgstr "匿名アカウントはこのプロパティではサポートされていません" + +#: authentication/api/connection_token.py:323 msgid "Account not found" msgstr "アカウントが見つかりません" -#: authentication/api/connection_token.py:308 +#: authentication/api/connection_token.py:326 msgid "Permission expired" msgstr "承認の有効期限が切れています" -#: authentication/api/connection_token.py:322 +#: authentication/api/connection_token.py:340 msgid "ACL action is reject: {}({})" msgstr "ACL アクションは拒否です: {}({})" -#: authentication/api/connection_token.py:326 +#: authentication/api/connection_token.py:344 msgid "ACL action is review" msgstr "ACL アクションはレビューです" @@ -2148,16 +2270,16 @@ msgstr "ACL アクションはレビューです" msgid "Current user not support mfa type: {}" msgstr "現在のユーザーはmfaタイプをサポートしていません: {}" -#: authentication/api/password.py:31 terminal/api/session/session.py:259 +#: authentication/api/password.py:32 terminal/api/session/session.py:259 #: users/views/profile/reset.py:44 msgid "User does not exist: {}" msgstr "ユーザーが存在しない: {}" -#: authentication/api/password.py:31 users/views/profile/reset.py:127 +#: authentication/api/password.py:32 users/views/profile/reset.py:127 msgid "No user matched" msgstr "ユーザーにマッチしなかった" -#: authentication/api/password.py:35 +#: authentication/api/password.py:36 msgid "" "The user is from {}, please go to the corresponding system to change the " "password" @@ -2165,7 +2287,7 @@ msgstr "" "ユーザーは {}からです。対応するシステムにアクセスしてパスワードを変更してくだ" "さい。" -#: authentication/api/password.py:59 +#: authentication/api/password.py:60 #: authentication/templates/authentication/login.html:305 #: users/templates/users/forgot_password.html:27 #: users/templates/users/forgot_password.html:28 @@ -2501,7 +2623,7 @@ msgstr "電話番号を設定して有効にする" msgid "Clear phone number to disable" msgstr "無効にする電話番号をクリアする" -#: authentication/middleware.py:93 settings/utils/ldap.py:657 +#: authentication/middleware.py:93 settings/utils/ldap.py:661 msgid "Authentication failed (before login check failed): {}" msgstr "認証に失敗しました (ログインチェックが失敗する前): {}" @@ -2521,78 +2643,86 @@ msgstr "MFAタイプ ({}) が有効になっていない" msgid "Please change your password" msgstr "パスワードを変更してください" -#: authentication/models/connection_token.py:37 +#: authentication/models/connection_token.py:38 #: terminal/serializers/storage.py:111 msgid "Account name" msgstr "アカウント名" -#: authentication/models/connection_token.py:38 +#: authentication/models/connection_token.py:39 msgid "Input username" msgstr "カスタム ユーザー名" -#: authentication/models/connection_token.py:39 -#: authentication/serializers/connection_token.py:20 +#: authentication/models/connection_token.py:40 +#: authentication/serializers/connection_token.py:18 msgid "Input secret" msgstr "カスタムパスワード" -#: authentication/models/connection_token.py:41 +#: authentication/models/connection_token.py:42 msgid "Connect method" msgstr "接続方法" -#: authentication/models/connection_token.py:42 +#: authentication/models/connection_token.py:43 msgid "Connect options" msgstr "接続アイテム" -#: authentication/models/connection_token.py:43 +#: authentication/models/connection_token.py:44 #: rbac/serializers/rolebinding.py:21 msgid "User display" msgstr "ユーザー表示" -#: authentication/models/connection_token.py:44 +#: authentication/models/connection_token.py:45 msgid "Asset display" msgstr "アセット名" -#: authentication/models/connection_token.py:45 +#: authentication/models/connection_token.py:46 msgid "Reusable" msgstr "再利用可能" -#: authentication/models/connection_token.py:46 +#: authentication/models/connection_token.py:47 #: authentication/models/temp_token.py:13 perms/models/asset_permission.py:74 #: tickets/models/ticket/apply_application.py:31 #: tickets/models/ticket/apply_asset.py:20 users/models/user.py:797 msgid "Date expired" msgstr "期限切れの日付" -#: authentication/models/connection_token.py:50 +#: authentication/models/connection_token.py:51 #: perms/models/asset_permission.py:77 msgid "From ticket" msgstr "チケットから" -#: authentication/models/connection_token.py:56 +#: authentication/models/connection_token.py:58 +msgid "Can expire connection token" +msgstr "接続トークンの有効期限を設定できます" + +#: authentication/models/connection_token.py:59 +msgid "Can reuse connection token" +msgstr "接続トークンを再利用できます" + +#: authentication/models/connection_token.py:61 msgid "Connection token" msgstr "接続トークン" -#: authentication/models/connection_token.py:58 -msgid "Can view connection token secret" -msgstr "接続トークンの秘密を表示できます" - -#: authentication/models/connection_token.py:105 +#: authentication/models/connection_token.py:116 msgid "Connection token inactive" msgstr "接続トークンがアクティブ化されていません" -#: authentication/models/connection_token.py:108 +#: authentication/models/connection_token.py:119 msgid "Connection token expired at: {}" msgstr "接続トークンの有効期限: {}" -#: authentication/models/connection_token.py:111 +#: authentication/models/connection_token.py:122 msgid "No user or invalid user" msgstr "ユーザーなしまたは期限切れのユーザー" -#: authentication/models/connection_token.py:114 +#: authentication/models/connection_token.py:125 msgid "No asset or inactive asset" msgstr "アセットがないか、有効化されていないアセット" -#: authentication/models/connection_token.py:267 +#: authentication/models/connection_token.py:269 +msgid "Can view super connection token secret" +msgstr "スーパー接続トークンのシークレットを表示できます" + +#: authentication/models/connection_token.py:271 msgid "Super connection token" msgstr "スーパー接続トークン" @@ -2636,15 +2766,15 @@ msgstr "コンポーネント" msgid "Expired now" msgstr "すぐに期限切れ" -#: authentication/serializers/connection_token.py:18 +#: authentication/serializers/connection_token.py:16 msgid "Expired time" msgstr "期限切れ時間" -#: authentication/serializers/connection_token.py:22 +#: authentication/serializers/connection_token.py:20 msgid "Ticket info" msgstr "作業指示情報" -#: authentication/serializers/connection_token.py:23 +#: authentication/serializers/connection_token.py:21 #: perms/models/asset_permission.py:71 perms/serializers/permission.py:36 #: perms/serializers/permission.py:56 #: tickets/models/ticket/apply_application.py:28 @@ -2652,18 +2782,12 @@ msgstr "作業指示情報" msgid "Actions" msgstr "アクション" -#: authentication/serializers/connection_token.py:44 +#: authentication/serializers/connection_token.py:42 #: perms/serializers/permission.py:38 perms/serializers/permission.py:57 -#: users/serializers/user.py:97 users/serializers/user.py:173 +#: users/serializers/user.py:96 users/serializers/user.py:172 msgid "Is expired" msgstr "期限切れです" -#: authentication/serializers/connection_token.py:79 -msgid "Reusable connection token is not allowed, global setting not enabled" -msgstr "" -"再使用可能な接続トークンの使用は許可されていません。グローバル設定は有効に" -"なっていません" - #: authentication/serializers/password_mfa.py:16 #: authentication/serializers/password_mfa.py:24 #: notifications/backends/__init__.py:10 settings/serializers/email.py:19 @@ -2680,8 +2804,8 @@ msgid "The {} cannot be empty" msgstr "{} 空にしてはならない" #: authentication/serializers/token.py:79 perms/serializers/permission.py:37 -#: perms/serializers/permission.py:58 users/serializers/user.py:98 -#: users/serializers/user.py:171 +#: perms/serializers/permission.py:58 users/serializers/user.py:97 +#: users/serializers/user.py:170 msgid "Is valid" msgstr "有効です" @@ -2758,7 +2882,7 @@ msgstr "コードエラー" #: authentication/templates/authentication/_msg_reset_password_code.html:9 #: authentication/templates/authentication/_msg_rest_password_success.html:2 #: authentication/templates/authentication/_msg_rest_public_key_success.html:2 -#: jumpserver/conf.py:426 +#: jumpserver/conf.py:431 #: perms/templates/perms/_msg_item_permissions_expire.html:3 #: perms/templates/perms/_msg_permed_items_expire.html:3 #: tickets/templates/tickets/approve_check_password.html:33 @@ -2906,7 +3030,7 @@ msgid "Copy success" msgstr "コピー成功" #: authentication/utils.py:28 common/utils/ip/geoip/utils.py:24 -#: xpack/plugins/cloud/const.py:27 +#: xpack/plugins/cloud/const.py:29 msgid "LAN" msgstr "ローカルエリアネットワーク" @@ -2981,23 +3105,23 @@ msgstr "本を飛ばすのバインドに成功" msgid "Failed to get user from FeiShu" msgstr "本を飛ばすからユーザーを取得できませんでした" -#: authentication/views/login.py:182 +#: authentication/views/login.py:183 msgid "Redirecting" msgstr "リダイレクト" -#: authentication/views/login.py:183 +#: authentication/views/login.py:184 msgid "Redirecting to {} authentication" msgstr "{} 認証へのリダイレクト" -#: authentication/views/login.py:206 -msgid "Please enable cookies and try again." -msgstr "クッキーを有効にして、もう一度お試しください。" +#: authentication/views/login.py:207 +msgid "Login timeout, please try again." +msgstr "ログインタイムアウト、もう一度お試しください" -#: authentication/views/login.py:247 +#: authentication/views/login.py:250 msgid "User email already exists ({})" msgstr "ユーザー メールボックスは既に存在します ({})" -#: authentication/views/login.py:325 +#: authentication/views/login.py:328 msgid "" "Wait for {} confirm, You also can copy link to her/him
\n" " Don't close this page" @@ -3005,15 +3129,15 @@ msgstr "" "{} 確認を待ちます。彼女/彼へのリンクをコピーすることもできます
\n" " このページを閉じないでください" -#: authentication/views/login.py:330 +#: authentication/views/login.py:333 msgid "No ticket found" msgstr "チケットが見つかりません" -#: authentication/views/login.py:366 +#: authentication/views/login.py:369 msgid "Logout success" msgstr "ログアウト成功" -#: authentication/views/login.py:367 +#: authentication/views/login.py:370 msgid "Logout success, return login page" msgstr "ログアウト成功、ログインページを返す" @@ -3061,7 +3185,7 @@ msgstr "タイミングトリガー" msgid "Ready" msgstr "の準備を" -#: common/const/choices.py:16 terminal/const.py:59 tickets/const.py:29 +#: common/const/choices.py:16 terminal/const.py:75 tickets/const.py:29 #: tickets/const.py:39 msgid "Pending" msgstr "未定" @@ -3116,7 +3240,7 @@ msgstr "テキストフィールドへのマーシャルデータ" msgid "Encrypt field using Secret Key" msgstr "Secret Keyを使用したフィールドの暗号化" -#: common/db/fields.py:558 +#: common/db/fields.py:567 msgid "" "Invalid JSON data for JSONManyToManyField, should be like {'type': 'all'} or " "{'type': 'ids', 'ids': []} or {'type': 'attrs', 'attrs': [{'name': 'ip', " @@ -3126,19 +3250,19 @@ msgstr "" "{'type':'ids','ids':[]}或 #タイプ:属性、属性:[#名前:ip、照合:正確、" "値:1.1.1.1}" -#: common/db/fields.py:565 +#: common/db/fields.py:574 msgid "Invalid type, should be \"all\", \"ids\" or \"attrs\"" msgstr "無効なタイプです。all、ids、またはattrsでなければなりません" -#: common/db/fields.py:568 +#: common/db/fields.py:577 msgid "Invalid ids for ids, should be a list" msgstr "無効なID、リストでなければなりません" -#: common/db/fields.py:573 common/db/fields.py:578 +#: common/db/fields.py:582 common/db/fields.py:587 msgid "Invalid attrs, should be a list of dict" msgstr "無効な属性、dictリストでなければなりません" -#: common/db/fields.py:580 +#: common/db/fields.py:589 msgid "Invalid attrs, should be has name and value" msgstr "名前と値が必要な無効な属性" @@ -3170,7 +3294,7 @@ msgstr "組織 ID" msgid "The file content overflowed (The maximum length `{}` bytes)" msgstr "ファイルの内容がオーバーフローしました (最大長 '{}' バイト)" -#: common/drf/parsers/base.py:193 +#: common/drf/parsers/base.py:195 msgid "Parse file error: {}" msgstr "解析ファイルエラー: {}" @@ -3352,11 +3476,11 @@ msgstr "検索のエクスポート: %s" msgid "User %s view/export secret" msgstr "ユーザー %s がパスワードを閲覧/導き出しました" -#: jumpserver/conf.py:425 +#: jumpserver/conf.py:430 msgid "Create account successfully" msgstr "アカウントを正常に作成" -#: jumpserver/conf.py:427 +#: jumpserver/conf.py:432 msgid "Your account has been created successfully" msgstr "アカウントが正常に作成されました" @@ -3448,11 +3572,11 @@ msgstr "タスクは存在しません" msgid "Task {} args or kwargs error" msgstr "タスク実行パラメータエラー" -#: ops/api/playbook.py:38 +#: ops/api/playbook.py:37 msgid "Currently playbook is being used in a job" msgstr "現在プレイブックは1つのジョブで使用されています" -#: ops/api/playbook.py:92 +#: ops/api/playbook.py:91 msgid "Unsupported file content" msgstr "サポートされていないファイルの内容" @@ -3496,7 +3620,7 @@ msgstr "VCS" msgid "Adhoc" msgstr "コマンド#コマンド#" -#: ops/const.py:39 ops/models/job.py:101 +#: ops/const.py:39 ops/models/job.py:103 msgid "Playbook" msgstr "Playbook" @@ -3553,17 +3677,17 @@ msgstr "定期的または定期的に設定を行う必要があります" msgid "Pattern" msgstr "パターン" -#: ops/models/adhoc.py:24 ops/models/job.py:96 +#: ops/models/adhoc.py:24 ops/models/job.py:98 msgid "Module" msgstr "モジュール" -#: ops/models/adhoc.py:25 ops/models/celery.py:58 ops/models/job.py:95 +#: ops/models/adhoc.py:25 ops/models/celery.py:58 ops/models/job.py:97 #: terminal/models/component/task.py:16 msgid "Args" msgstr "アルグ" #: ops/models/adhoc.py:26 ops/models/base.py:16 ops/models/base.py:53 -#: ops/models/job.py:104 ops/models/job.py:190 ops/models/playbook.py:25 +#: ops/models/job.py:106 ops/models/job.py:192 ops/models/playbook.py:25 #: terminal/models/session/sharing.py:23 msgid "Creator" msgstr "作成者" @@ -3580,12 +3704,12 @@ msgstr "最後の実行" msgid "Date last run" msgstr "最終実行日" -#: ops/models/base.py:51 ops/models/job.py:188 +#: ops/models/base.py:51 ops/models/job.py:190 #: xpack/plugins/cloud/models.py:162 msgid "Result" msgstr "結果" -#: ops/models/base.py:52 ops/models/job.py:189 +#: ops/models/base.py:52 ops/models/job.py:191 msgid "Summary" msgstr "概要" @@ -3618,43 +3742,43 @@ msgstr "発売日" msgid "Celery Task Execution" msgstr "Celery タスク実行" -#: ops/models/job.py:98 +#: ops/models/job.py:100 msgid "Chdir" msgstr "実行ディレクトリ" -#: ops/models/job.py:99 +#: ops/models/job.py:101 msgid "Timeout (Seconds)" msgstr "タイムアウト(秒)" -#: ops/models/job.py:106 +#: ops/models/job.py:108 msgid "Use Parameter Define" msgstr "パラメータ定義を使用する" -#: ops/models/job.py:107 +#: ops/models/job.py:109 msgid "Parameters define" msgstr "パラメータ定義" -#: ops/models/job.py:108 +#: ops/models/job.py:110 msgid "Runas" msgstr "ユーザーとして実行" -#: ops/models/job.py:110 +#: ops/models/job.py:112 msgid "Runas policy" msgstr "ユーザー ポリシー" -#: ops/models/job.py:172 +#: ops/models/job.py:174 msgid "Job" msgstr "ジョブ#ジョブ#" -#: ops/models/job.py:195 +#: ops/models/job.py:197 msgid "Material" msgstr "Material" -#: ops/models/job.py:197 +#: ops/models/job.py:199 msgid "Material Type" msgstr "Material を選択してオプションを設定します。" -#: ops/models/job.py:460 +#: ops/models/job.py:480 msgid "Job Execution" msgstr "ジョブ実行" @@ -3702,7 +3826,7 @@ msgstr "保存後に実行" msgid "Job type" msgstr "タスクの種類" -#: ops/serializers/job.py:57 terminal/serializers/session.py:50 +#: ops/serializers/job.py:57 terminal/serializers/session.py:47 msgid "Is finished" msgstr "終了しました" @@ -3793,13 +3917,14 @@ msgstr "" msgid "The organization have resource ({}) cannot be deleted" msgstr "組織のリソース ({}) は削除できません" -#: orgs/apps.py:7 rbac/tree.py:118 +#: orgs/apps.py:7 rbac/tree.py:119 msgid "App organizations" msgstr "アプリ組織" #: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:89 #: rbac/const.py:7 rbac/models/rolebinding.py:56 #: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:63 +#: terminal/templates/terminal/_msg_command_warning.html:21 #: tickets/models/ticket/general.py:302 tickets/serializers/ticket/ticket.py:60 msgid "Organization" msgstr "組織" @@ -3916,7 +4041,7 @@ msgstr "組織 {} の資産権限" msgid "Check asset permission expired" msgstr "アセット認証ルールの有効期限が切れていることを確認する" -#: perms/tasks.py:39 +#: perms/tasks.py:40 msgid "Send asset permission expired notification" msgstr "アセット許可の有効期限通知を送信する" @@ -3952,27 +4077,27 @@ msgstr "{} 少なくとも1つのシステムロール" msgid "RBAC" msgstr "RBAC" -#: rbac/builtin.py:113 +#: rbac/builtin.py:114 msgid "SystemAdmin" msgstr "システム管理者" -#: rbac/builtin.py:116 +#: rbac/builtin.py:117 msgid "SystemAuditor" msgstr "システム監査人" -#: rbac/builtin.py:119 +#: rbac/builtin.py:120 msgid "SystemComponent" msgstr "システムコンポーネント" -#: rbac/builtin.py:125 +#: rbac/builtin.py:126 msgid "OrgAdmin" msgstr "組織管理者" -#: rbac/builtin.py:128 +#: rbac/builtin.py:129 msgid "OrgAuditor" msgstr "監査員を組織する" -#: rbac/builtin.py:131 +#: rbac/builtin.py:132 msgid "OrgUser" msgstr "組織ユーザー" @@ -4118,24 +4243,24 @@ msgid "My assets" msgstr "私の資産" #: rbac/tree.py:56 terminal/models/applet/applet.py:51 -#: terminal/models/applet/applet.py:240 terminal/models/applet/host.py:28 +#: terminal/models/applet/applet.py:244 terminal/models/applet/host.py:28 #: terminal/serializers/applet.py:15 msgid "Applet" msgstr "リモートアプリケーション" -#: rbac/tree.py:119 +#: rbac/tree.py:120 msgid "Ticket comment" msgstr "チケットコメント" -#: rbac/tree.py:120 tickets/models/ticket/general.py:307 +#: rbac/tree.py:121 tickets/models/ticket/general.py:307 msgid "Ticket" msgstr "チケット" -#: rbac/tree.py:121 +#: rbac/tree.py:122 msgid "Common setting" msgstr "共通設定" -#: rbac/tree.py:122 +#: rbac/tree.py:123 msgid "View permission tree" msgstr "権限ツリーの表示" @@ -4148,23 +4273,23 @@ msgstr "テストの成功" msgid "Test mail sent to {}, please check" msgstr "{}に送信されたテストメールを確認してください" -#: settings/api/ldap.py:173 +#: settings/api/ldap.py:176 msgid "Synchronization start, please wait." msgstr "同期開始、お待ちください。" -#: settings/api/ldap.py:177 +#: settings/api/ldap.py:180 msgid "Synchronization is running, please wait." msgstr "同期が実行中です。しばらくお待ちください。" -#: settings/api/ldap.py:182 +#: settings/api/ldap.py:185 msgid "Synchronization error: {}" msgstr "同期エラー: {}" -#: settings/api/ldap.py:220 +#: settings/api/ldap.py:223 msgid "Get ldap users is None" msgstr "Ldapユーザーを取得するにはNone" -#: settings/api/ldap.py:230 +#: settings/api/ldap.py:233 msgid "Imported {} users successfully (Organization: {})" msgstr "{} 人のユーザーを正常にインポートしました (組織: {})" @@ -5078,10 +5203,6 @@ msgstr "ログインcaptchaの有効化" msgid "Enable captcha to prevent robot authentication" msgstr "Captchaを有効にしてロボット認証を防止する" -#: settings/serializers/security.py:151 -msgid "Security" -msgstr "セキュリティ" - #: settings/serializers/security.py:154 msgid "Enable terminal register" msgstr "ターミナルレジスタの有効化" @@ -5241,104 +5362,104 @@ msgstr "" msgid "Periodic import ldap user" msgstr "LDAP ユーザーを定期的にインポートする" -#: settings/tasks/ldap.py:47 +#: settings/tasks/ldap.py:46 msgid "Registration periodic import ldap user task" msgstr "登録サイクルLDAPユーザータスクのインポート" -#: settings/utils/ldap.py:472 +#: settings/utils/ldap.py:476 msgid "ldap:// or ldaps:// protocol is used." msgstr "ldap:// または ldaps:// プロトコルが使用されます。" -#: settings/utils/ldap.py:483 +#: settings/utils/ldap.py:487 msgid "Host or port is disconnected: {}" msgstr "ホストまたはポートが切断されました: {}" -#: settings/utils/ldap.py:485 +#: settings/utils/ldap.py:489 msgid "The port is not the port of the LDAP service: {}" msgstr "ポートはLDAPサービスのポートではありません: {}" -#: settings/utils/ldap.py:487 +#: settings/utils/ldap.py:491 msgid "Please add certificate: {}" msgstr "証明書を追加してください: {}" -#: settings/utils/ldap.py:491 settings/utils/ldap.py:518 -#: settings/utils/ldap.py:548 settings/utils/ldap.py:576 +#: settings/utils/ldap.py:495 settings/utils/ldap.py:522 +#: settings/utils/ldap.py:552 settings/utils/ldap.py:580 msgid "Unknown error: {}" msgstr "不明なエラー: {}" -#: settings/utils/ldap.py:505 +#: settings/utils/ldap.py:509 msgid "Bind DN or Password incorrect" msgstr "DNまたはパスワードのバインドが正しくありません" -#: settings/utils/ldap.py:512 +#: settings/utils/ldap.py:516 msgid "Please enter Bind DN: {}" msgstr "バインドDN: {} を入力してください" -#: settings/utils/ldap.py:514 +#: settings/utils/ldap.py:518 msgid "Please enter Password: {}" msgstr "パスワードを入力してください: {}" -#: settings/utils/ldap.py:516 +#: settings/utils/ldap.py:520 msgid "Please enter correct Bind DN and Password: {}" msgstr "正しいバインドDNとパスワードを入力してください: {}" -#: settings/utils/ldap.py:534 +#: settings/utils/ldap.py:538 msgid "Invalid User OU or User search filter: {}" msgstr "無効なユーザー OU またはユーザー検索フィルター: {}" -#: settings/utils/ldap.py:565 +#: settings/utils/ldap.py:569 msgid "LDAP User attr map not include: {}" msgstr "LDAP ユーザーattrマップは含まれません: {}" -#: settings/utils/ldap.py:572 +#: settings/utils/ldap.py:576 msgid "LDAP User attr map is not dict" msgstr "LDAPユーザーattrマップはdictではありません" -#: settings/utils/ldap.py:591 +#: settings/utils/ldap.py:595 msgid "LDAP authentication is not enabled" msgstr "LDAP 認証が有効になっていない" -#: settings/utils/ldap.py:609 +#: settings/utils/ldap.py:613 msgid "Error (Invalid LDAP server): {}" msgstr "エラー (LDAPサーバーが無効): {}" -#: settings/utils/ldap.py:611 +#: settings/utils/ldap.py:615 msgid "Error (Invalid Bind DN): {}" msgstr "エラー (DNのバインドが無効): {}" -#: settings/utils/ldap.py:613 +#: settings/utils/ldap.py:617 msgid "Error (Invalid LDAP User attr map): {}" msgstr "エラー (LDAPユーザーattrマップが無効): {}" -#: settings/utils/ldap.py:615 +#: settings/utils/ldap.py:619 msgid "Error (Invalid User OU or User search filter): {}" msgstr "エラー (ユーザーOUまたはユーザー検索フィルターが無効): {}" -#: settings/utils/ldap.py:617 +#: settings/utils/ldap.py:621 msgid "Error (Not enabled LDAP authentication): {}" msgstr "エラー (LDAP認証が有効化されていません): {}" -#: settings/utils/ldap.py:619 +#: settings/utils/ldap.py:623 msgid "Error (Unknown): {}" msgstr "エラー (不明): {}" -#: settings/utils/ldap.py:622 +#: settings/utils/ldap.py:626 msgid "Succeed: Match {} s user" msgstr "成功: {} 人のユーザーに一致" -#: settings/utils/ldap.py:655 +#: settings/utils/ldap.py:659 msgid "Authentication failed (configuration incorrect): {}" msgstr "認証に失敗しました (設定が正しくありません): {}" -#: settings/utils/ldap.py:659 +#: settings/utils/ldap.py:663 msgid "Authentication failed (username or password incorrect): {}" msgstr "認証に失敗しました (ユーザー名またはパスワードが正しくありません): {}" -#: settings/utils/ldap.py:661 +#: settings/utils/ldap.py:665 msgid "Authentication failed (Unknown): {}" msgstr "認証に失敗しました (不明): {}" -#: settings/utils/ldap.py:664 +#: settings/utils/ldap.py:668 msgid "Authentication success: {}" msgstr "認証成功: {}" @@ -5579,7 +5700,7 @@ msgstr "テスト失敗: {}" msgid "Test successful" msgstr "テスト成功" -#: terminal/api/component/storage.py:124 terminal/notifications.py:179 +#: terminal/api/component/storage.py:124 terminal/notifications.py:240 #: terminal/tasks.py:144 msgid "Test failure: Account invalid" msgstr "テスト失敗: アカウントが無効" @@ -5608,60 +5729,69 @@ msgstr "安全なセッション共有設定が無効になっています" msgid "Terminals" msgstr "ターミナル管理" -#: terminal/backends/command/models.py:15 -msgid "Ordinary" -msgstr "普通" - -#: terminal/backends/command/models.py:16 -msgid "Dangerous" -msgstr "危険" - -#: terminal/backends/command/models.py:22 +#: terminal/backends/command/models.py:20 msgid "Input" msgstr "入力" -#: terminal/backends/command/models.py:23 terminal/serializers/command.py:48 +#: terminal/backends/command/models.py:21 terminal/serializers/command.py:74 msgid "Output" msgstr "出力" -#: terminal/backends/command/models.py:27 terminal/serializers/command.py:22 +#: terminal/backends/command/models.py:25 terminal/serializers/command.py:23 +#: terminal/templates/terminal/_msg_command_warning.html:10 msgid "Risk level" msgstr "リスクレベル" -#: terminal/connect_methods.py:55 +#: terminal/connect_methods.py:34 msgid "DB Client" msgstr "データベース クライアント" -#: terminal/const.py:30 +#: terminal/const.py:12 +msgid "Review & Reject" +msgstr "レビューと拒否" + +#: terminal/const.py:13 +msgid "Review & Accept" +msgstr "確認して同意する" + +#: terminal/const.py:14 +msgid "Review & Cancel" +msgstr "確認してキャンセル" + +#: terminal/const.py:44 msgid "Critical" msgstr "クリティカル" -#: terminal/const.py:31 +#: terminal/const.py:45 msgid "High" msgstr "高い" -#: terminal/const.py:32 terminal/const.py:66 +#: terminal/const.py:46 terminal/const.py:82 #: users/templates/users/reset_password.html:50 msgid "Normal" msgstr "正常" -#: terminal/const.py:33 +#: terminal/const.py:47 msgid "Offline" msgstr "オフライン" -#: terminal/const.py:62 +#: terminal/const.py:78 msgid "Mismatch" msgstr "一致しない" -#: terminal/const.py:67 +#: terminal/const.py:83 msgid "Tunnel" msgstr "" -#: terminal/const.py:72 +#: terminal/const.py:85 +msgid "SFTP" +msgstr "SFTP" + +#: terminal/const.py:89 msgid "Read Only" msgstr "読み取り専用" -#: terminal/const.py:73 +#: terminal/const.py:90 msgid "Writable" msgstr "書き込み可能" @@ -5717,7 +5847,7 @@ msgstr "カスタムプラットフォームのみをサポート" msgid "Missing type in platform.yml" msgstr "platform.ymlにタイプがありません" -#: terminal/models/applet/applet.py:242 terminal/models/applet/host.py:34 +#: terminal/models/applet/applet.py:246 terminal/models/applet/host.py:34 #: terminal/models/applet/host.py:134 msgid "Hosting" msgstr "ホスト マシン" @@ -5775,18 +5905,18 @@ msgid "Redis port" msgstr "Redis ポート" #: terminal/models/component/endpoint.py:29 -#: terminal/models/component/endpoint.py:98 terminal/serializers/endpoint.py:73 -#: terminal/serializers/storage.py:38 terminal/serializers/storage.py:50 -#: terminal/serializers/storage.py:80 terminal/serializers/storage.py:90 -#: terminal/serializers/storage.py:98 +#: terminal/models/component/endpoint.py:100 +#: terminal/serializers/endpoint.py:73 terminal/serializers/storage.py:38 +#: terminal/serializers/storage.py:50 terminal/serializers/storage.py:80 +#: terminal/serializers/storage.py:90 terminal/serializers/storage.py:98 msgid "Endpoint" msgstr "エンドポイント" -#: terminal/models/component/endpoint.py:91 +#: terminal/models/component/endpoint.py:93 msgid "IP group" msgstr "IP グループ" -#: terminal/models/component/endpoint.py:103 +#: terminal/models/component/endpoint.py:106 msgid "Endpoint rule" msgstr "エンドポイントルール" @@ -5836,7 +5966,7 @@ msgstr "再生ストレージ" msgid "type" msgstr "タイプ" -#: terminal/models/component/terminal.py:89 terminal/serializers/command.py:51 +#: terminal/models/component/terminal.py:89 terminal/serializers/command.py:77 msgid "Remote Address" msgstr "リモートアドレス" @@ -5864,39 +5994,39 @@ msgstr "セッションのリプレイをアップロードできます" msgid "Can download session replay" msgstr "セッション再生をダウンロードできます" -#: terminal/models/session/session.py:35 +#: terminal/models/session/session.py:34 msgid "Account id" msgstr "アカウント ID" -#: terminal/models/session/session.py:37 terminal/models/session/sharing.py:104 +#: terminal/models/session/session.py:36 terminal/models/session/sharing.py:104 msgid "Login from" msgstr "ログイン元" -#: terminal/models/session/session.py:42 +#: terminal/models/session/session.py:41 msgid "Replay" msgstr "リプレイ" -#: terminal/models/session/session.py:46 +#: terminal/models/session/session.py:45 msgid "Date end" msgstr "終了日" -#: terminal/models/session/session.py:243 +#: terminal/models/session/session.py:240 msgid "Session record" msgstr "セッション記録" -#: terminal/models/session/session.py:245 +#: terminal/models/session/session.py:242 msgid "Can monitor session" msgstr "セッションを監視できます" -#: terminal/models/session/session.py:246 +#: terminal/models/session/session.py:243 msgid "Can share session" msgstr "セッションを共有できます" -#: terminal/models/session/session.py:247 +#: terminal/models/session/session.py:244 msgid "Can terminate session" msgstr "セッションを終了できます" -#: terminal/models/session/session.py:248 +#: terminal/models/session/session.py:245 msgid "Can validate session action perm" msgstr "セッションアクションのパーマを検証できます" @@ -5953,31 +6083,35 @@ msgstr "検証コードが無効" msgid "You have already joined this session" msgstr "すでにこのセッションに参加しています" -#: terminal/notifications.py:21 +#: terminal/notifications.py:25 msgid "Sessions" msgstr "セッション" -#: terminal/notifications.py:67 -msgid "Danger command alert" -msgstr "危険コマンドアラート" +#: terminal/notifications.py:72 +msgid "Command warning" +msgstr "コマンド警告" -#: terminal/notifications.py:97 terminal/notifications.py:145 +#: terminal/notifications.py:130 +msgid "Command reject" +msgstr "コマンド拒否" + +#: terminal/notifications.py:157 terminal/notifications.py:206 msgid "Level" msgstr "レベル" -#: terminal/notifications.py:115 +#: terminal/notifications.py:175 msgid "Batch danger command alert" msgstr "一括危険コマンド警告" -#: terminal/notifications.py:163 +#: terminal/notifications.py:224 msgid "Command and replay storage" msgstr "コマンド及び録画記憶" -#: terminal/notifications.py:164 +#: terminal/notifications.py:225 msgid "Connectivity alarm" msgstr "接続性アラーム" -#: terminal/notifications.py:189 +#: terminal/notifications.py:250 #: terminal/templates/terminal/_msg_check_command_replay_storage_connectivity.html:4 msgid "Invalid storage" msgstr "無効なストレージ" @@ -6049,15 +6183,35 @@ msgstr "RDS 远程应用注销时间限制" msgid "Load status" msgstr "ロードステータス" -#: terminal/serializers/command.py:19 +#: terminal/serializers/command.py:20 msgid "Session ID" msgstr "セッションID" -#: terminal/serializers/command.py:47 +#: terminal/serializers/command.py:42 +msgid "Command Filter ACL" +msgstr "コマンドフィルター" + +#: terminal/serializers/command.py:45 +msgid "Command Group" +msgstr "コマンドグループ" + +#: terminal/serializers/command.py:56 +msgid "Invalid command filter ACL id" +msgstr "無効なコマンドフィルターID" + +#: terminal/serializers/command.py:60 +msgid "Invalid command group id" +msgstr "無効なコマンドグループID" + +#: terminal/serializers/command.py:64 +msgid "Invalid session id" +msgstr "無効なセッションID" + +#: terminal/serializers/command.py:73 msgid "Account " msgstr "アカウント" -#: terminal/serializers/command.py:49 +#: terminal/serializers/command.py:75 msgid "Timestamp" msgstr "タイムスタンプ" @@ -6104,35 +6258,35 @@ msgstr "" msgid "Asset IP" msgstr "資産 IP" -#: terminal/serializers/session.py:25 terminal/serializers/session.py:47 +#: terminal/serializers/session.py:22 terminal/serializers/session.py:44 msgid "Can replay" msgstr "再生できます" -#: terminal/serializers/session.py:26 terminal/serializers/session.py:48 +#: terminal/serializers/session.py:23 terminal/serializers/session.py:45 msgid "Can join" msgstr "参加できます" -#: terminal/serializers/session.py:27 terminal/serializers/session.py:51 +#: terminal/serializers/session.py:24 terminal/serializers/session.py:48 msgid "Can terminate" msgstr "終了できます" -#: terminal/serializers/session.py:43 +#: terminal/serializers/session.py:40 msgid "User ID" msgstr "ユーザーID" -#: terminal/serializers/session.py:44 +#: terminal/serializers/session.py:41 msgid "Asset ID" msgstr "資産ID" -#: terminal/serializers/session.py:45 +#: terminal/serializers/session.py:42 msgid "Login from display" msgstr "表示からのログイン" -#: terminal/serializers/session.py:52 +#: terminal/serializers/session.py:49 msgid "Terminal display" msgstr "ターミナルディスプレイ" -#: terminal/serializers/session.py:57 +#: terminal/serializers/session.py:54 msgid "Command amount" msgstr "コマンド量" @@ -6234,7 +6388,7 @@ msgstr "チェックコマンドと録画ストレージの接続性" msgid "view" msgstr "表示" -#: terminal/utils/db_port_mapper.py:84 +#: terminal/utils/db_port_mapper.py:85 msgid "" "No available port is matched. The number of databases may have exceeded the " "number of ports open to the database agent service, Contact the " @@ -6244,7 +6398,7 @@ msgstr "" "サービスによって開かれたポートの数を超えた可能性があります。さらにポートを開" "くには、管理者に連絡してください。" -#: terminal/utils/db_port_mapper.py:112 +#: terminal/utils/db_port_mapper.py:113 msgid "" "No ports can be used, check and modify the limit on the number of ports that " "Magnus listens on in the configuration file." @@ -6252,7 +6406,7 @@ msgstr "" "使用できるポートがありません。設定ファイルで Magnus がリッスンするポート数の" "制限を確認して変更してください. " -#: terminal/utils/db_port_mapper.py:114 +#: terminal/utils/db_port_mapper.py:115 msgid "All available port count: {}, Already use port count: {}" msgstr "使用可能なすべてのポート数: {}、すでに使用しているポート数: {}" @@ -6447,11 +6601,11 @@ msgstr "承認ステップ" msgid "Relation snapshot" msgstr "製造オーダスナップショット" -#: tickets/models/ticket/general.py:392 +#: tickets/models/ticket/general.py:398 msgid "Please try again" msgstr "もう一度お試しください" -#: tickets/models/ticket/general.py:461 +#: tickets/models/ticket/general.py:467 msgid "Super ticket" msgstr "スーパーチケット" @@ -6528,7 +6682,7 @@ msgid "Apply actions" msgstr "申請アクション" #: tickets/serializers/ticket/common.py:15 -#: tickets/serializers/ticket/common.py:77 +#: tickets/serializers/ticket/common.py:75 msgid "Created by ticket ({}-{})" msgstr "チケットで作成 ({}-{})" @@ -6536,7 +6690,7 @@ msgstr "チケットで作成 ({}-{})" msgid "The expiration date should be greater than the start date" msgstr "有効期限は開始日より大きくする必要があります" -#: tickets/serializers/ticket/common.py:84 +#: tickets/serializers/ticket/common.py:82 msgid "Permission named `{}` already exists" msgstr "'{}'という名前の権限は既に存在します" @@ -6704,6 +6858,7 @@ msgid "Not a valid ssh public key" msgstr "有効なssh公開鍵ではありません" #: users/forms/profile.py:173 users/models/user.py:786 +#: xpack/plugins/cloud/serializers/account_attrs.py:206 msgid "Public key" msgstr "公開キー" @@ -6711,7 +6866,7 @@ msgstr "公開キー" msgid "Force enable" msgstr "強制有効" -#: users/models/user.py:765 users/serializers/user.py:172 +#: users/models/user.py:765 users/serializers/user.py:171 msgid "Is service account" msgstr "サービスアカウントです" @@ -6723,7 +6878,7 @@ msgstr "アバター" msgid "Wechat" msgstr "微信" -#: users/models/user.py:773 users/serializers/user.py:109 +#: users/models/user.py:773 users/serializers/user.py:108 msgid "Phone" msgstr "電話" @@ -6732,6 +6887,7 @@ msgid "OTP secret key" msgstr "OTP 秘密" #: users/models/user.py:783 +#: xpack/plugins/cloud/serializers/account_attrs.py:209 msgid "Private key" msgstr "ssh秘密鍵" @@ -6740,7 +6896,7 @@ msgid "Secret key" msgstr "秘密キー" #: users/models/user.py:794 users/serializers/profile.py:149 -#: users/serializers/user.py:169 +#: users/serializers/user.py:168 msgid "Is first login" msgstr "最初のログインです" @@ -6823,55 +6979,55 @@ msgstr "新しいパスワードを最後の {} 個のパスワードにする msgid "The newly set password is inconsistent" msgstr "新しく設定されたパスワードが一致しない" -#: users/serializers/user.py:43 +#: users/serializers/user.py:42 msgid "System roles" msgstr "システムの役割" -#: users/serializers/user.py:47 +#: users/serializers/user.py:46 msgid "Org roles" msgstr "組織ロール" -#: users/serializers/user.py:90 +#: users/serializers/user.py:89 msgid "Password strategy" msgstr "パスワード戦略" -#: users/serializers/user.py:92 +#: users/serializers/user.py:91 msgid "MFA enabled" msgstr "MFA有効化" -#: users/serializers/user.py:94 +#: users/serializers/user.py:93 msgid "MFA force enabled" msgstr "MFAフォース有効化" -#: users/serializers/user.py:96 +#: users/serializers/user.py:95 msgid "Login blocked" msgstr "ログインがロックされました" -#: users/serializers/user.py:99 users/serializers/user.py:177 +#: users/serializers/user.py:98 users/serializers/user.py:176 msgid "Is OTP bound" msgstr "仮想MFAがバインドされているか" -#: users/serializers/user.py:101 +#: users/serializers/user.py:100 msgid "Can public key authentication" msgstr "公開鍵認証が可能" -#: users/serializers/user.py:174 +#: users/serializers/user.py:173 msgid "Avatar url" msgstr "アバターURL" -#: users/serializers/user.py:178 +#: users/serializers/user.py:177 msgid "MFA level" msgstr "MFA レベル" -#: users/serializers/user.py:284 +#: users/serializers/user.py:283 msgid "Select users" msgstr "ユーザーの選択" -#: users/serializers/user.py:285 +#: users/serializers/user.py:284 msgid "For security, only list several users" msgstr "セキュリティのために、複数のユーザーのみをリストします" -#: users/serializers/user.py:318 +#: users/serializers/user.py:317 msgid "name not unique" msgstr "名前が一意ではない" @@ -7211,70 +7367,74 @@ msgid "Tencent Cloud (Lighthouse)" msgstr "テンセント雲(軽量アプリケーション)" #: xpack/plugins/cloud/const.py:19 -msgid "VMware" -msgstr "VMware" - -#: xpack/plugins/cloud/const.py:20 xpack/plugins/cloud/providers/nutanix.py:13 -msgid "Nutanix" -msgstr "Nutanix" - -#: xpack/plugins/cloud/const.py:21 -msgid "Huawei Private Cloud" -msgstr "華為私有雲" - -#: xpack/plugins/cloud/const.py:22 -msgid "Qingyun Private Cloud" -msgstr "青雲私有雲" - -#: xpack/plugins/cloud/const.py:23 -msgid "CTYun Private Cloud" -msgstr "スカイウィング私有雲" - -#: xpack/plugins/cloud/const.py:24 -msgid "OpenStack" -msgstr "OpenStack" - -#: xpack/plugins/cloud/const.py:25 msgid "Google Cloud Platform" msgstr "谷歌雲" +#: xpack/plugins/cloud/const.py:20 +msgid "UCloud" +msgstr "ucloud" + +#: xpack/plugins/cloud/const.py:22 +msgid "VMware" +msgstr "VMware" + +#: xpack/plugins/cloud/const.py:23 xpack/plugins/cloud/providers/nutanix.py:13 +msgid "Nutanix" +msgstr "Nutanix" + +#: xpack/plugins/cloud/const.py:24 +msgid "Huawei Private Cloud" +msgstr "華為私有雲" + +#: xpack/plugins/cloud/const.py:25 +msgid "Qingyun Private Cloud" +msgstr "青雲私有雲" + #: xpack/plugins/cloud/const.py:26 +msgid "CTYun Private Cloud" +msgstr "スカイウィング私有雲" + +#: xpack/plugins/cloud/const.py:27 +msgid "OpenStack" +msgstr "OpenStack" + +#: xpack/plugins/cloud/const.py:28 msgid "Fusion Compute" msgstr "融合計算" -#: xpack/plugins/cloud/const.py:31 +#: xpack/plugins/cloud/const.py:33 msgid "Private IP" msgstr "プライベートIP" -#: xpack/plugins/cloud/const.py:32 +#: xpack/plugins/cloud/const.py:34 msgid "Public IP" msgstr "パブリックIP" -#: xpack/plugins/cloud/const.py:36 +#: xpack/plugins/cloud/const.py:38 msgid "Instance name" msgstr "インスタンス名" -#: xpack/plugins/cloud/const.py:37 +#: xpack/plugins/cloud/const.py:39 msgid "Instance name and Partial IP" msgstr "インスタンス名と部分IP" -#: xpack/plugins/cloud/const.py:42 +#: xpack/plugins/cloud/const.py:44 msgid "Succeed" msgstr "成功" -#: xpack/plugins/cloud/const.py:46 +#: xpack/plugins/cloud/const.py:48 msgid "Unsync" msgstr "同期していません" -#: xpack/plugins/cloud/const.py:47 +#: xpack/plugins/cloud/const.py:49 msgid "New Sync" msgstr "新しい同期" -#: xpack/plugins/cloud/const.py:48 +#: xpack/plugins/cloud/const.py:50 msgid "Synced" msgstr "同期済み" -#: xpack/plugins/cloud/const.py:49 +#: xpack/plugins/cloud/const.py:51 msgid "Released" msgstr "リリース済み" @@ -7540,11 +7700,11 @@ msgstr "華南-広州-友好ユーザー環境" msgid "CN East-Suqian" msgstr "華東-宿遷" -#: xpack/plugins/cloud/serializers/account.py:63 +#: xpack/plugins/cloud/serializers/account.py:64 msgid "Validity display" msgstr "有効表示" -#: xpack/plugins/cloud/serializers/account.py:64 +#: xpack/plugins/cloud/serializers/account.py:65 msgid "Provider display" msgstr "プロバイダ表示" @@ -7564,6 +7724,7 @@ msgstr "サブスクリプションID" #: xpack/plugins/cloud/serializers/account_attrs.py:103 #: xpack/plugins/cloud/serializers/account_attrs.py:119 #: xpack/plugins/cloud/serializers/account_attrs.py:149 +#: xpack/plugins/cloud/serializers/account_attrs.py:202 msgid "API Endpoint" msgstr "APIエンドポイント" @@ -7629,6 +7790,10 @@ msgstr "テストポート" msgid "Test timeout" msgstr "テストタイムアウト" +#: xpack/plugins/cloud/serializers/account_attrs.py:212 +msgid "Project" +msgstr "project" + #: xpack/plugins/cloud/serializers/task.py:28 msgid "" "Only instances matching the IP range will be synced.
If the instance " @@ -7726,3 +7891,12 @@ msgstr "究極のエディション" #: xpack/plugins/license/models.py:86 msgid "Community edition" msgstr "コミュニティ版" + +#~ msgid "Item" +#~ msgstr "アイテム" + +#~ msgid "Url" +#~ msgstr "リンク" + +#~ msgid "Danger command alert" +#~ msgstr "危険コマンドアラート" diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index ef7c79319..14979766a 100644 --- a/apps/locale/zh/LC_MESSAGES/django.mo +++ b/apps/locale/zh/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f32e327dd50762b76d209f80b7de470df6faf5242af383dd6152b0c7ea5a7974 -size 119261 +oid sha256:1b88bc1c5216d7cfc2b0a72d889198bcab84ddd40dd3f5a13a5662dfcf8170ee +size 121846 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 3408c2622..8db571768 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-06-14 20:40+0800\n" +"POT-Creation-Date: 2023-07-20 18:40+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -27,7 +27,7 @@ msgstr "参数 'action' 必须是 [{}]" #: authentication/confirm/password.py:9 authentication/forms.py:32 #: authentication/templates/authentication/login.html:274 #: settings/serializers/auth/ldap.py:25 settings/serializers/auth/ldap.py:47 -#: users/forms/profile.py:22 users/serializers/user.py:105 +#: users/forms/profile.py:22 users/serializers/user.py:104 #: users/templates/users/_msg_user_created.html:13 #: users/templates/users/user_password_verify.html:18 #: xpack/plugins/cloud/serializers/account_attrs.py:28 @@ -48,45 +48,53 @@ msgstr "Access key" msgid "Token" msgstr "Token" -#: accounts/const/account.py:13 common/db/fields.py:244 +#: accounts/const/account.py:10 +msgid "API key" +msgstr "" + +#: accounts/const/account.py:14 common/db/fields.py:244 #: settings/serializers/terminal.py:14 msgid "All" msgstr "全部" -#: accounts/const/account.py:14 +#: accounts/const/account.py:15 msgid "Manual input" msgstr "手动输入" -#: accounts/const/account.py:15 +#: accounts/const/account.py:16 msgid "Dynamic user" msgstr "同名账号" -#: accounts/const/account.py:19 users/models/user.py:699 +#: accounts/const/account.py:17 +msgid "Anonymous account" +msgstr "匿名账号" + +#: accounts/const/account.py:21 users/models/user.py:699 msgid "Local" msgstr "数据库" -#: accounts/const/account.py:20 +#: accounts/const/account.py:22 msgid "Collected" msgstr "收集" -#: accounts/const/account.py:21 accounts/serializers/account/account.py:27 +#: accounts/const/account.py:23 accounts/serializers/account/account.py:27 #: settings/serializers/auth/sms.py:75 msgid "Template" msgstr "模板" -#: accounts/const/account.py:25 ops/const.py:45 +#: accounts/const/account.py:27 ops/const.py:45 msgid "Skip" msgstr "跳过" -#: accounts/const/account.py:26 audits/const.py:24 rbac/tree.py:229 +#: accounts/const/account.py:28 audits/const.py:24 rbac/tree.py:230 #: templates/_csv_import_export.html:18 templates/_csv_update_modal.html:6 msgid "Update" msgstr "更新" -#: accounts/const/account.py:27 +#: accounts/const/account.py:29 #: accounts/serializers/automations/change_secret.py:156 audits/const.py:54 #: audits/signal_handlers/activity_log.py:33 common/const/choices.py:19 -#: ops/const.py:58 terminal/const.py:61 xpack/plugins/cloud/const.py:41 +#: ops/const.py:58 terminal/const.py:77 xpack/plugins/cloud/const.py:43 msgid "Failed" msgstr "失败" @@ -185,18 +193,19 @@ msgstr "仅创建" #: accounts/serializers/account/gathered_account.py:10 #: accounts/serializers/automations/change_secret.py:112 #: accounts/serializers/automations/change_secret.py:132 -#: acls/serializers/base.py:118 assets/models/asset/common.py:93 -#: assets/models/asset/common.py:332 assets/models/cmd_filter.py:36 +#: acls/serializers/base.py:123 assets/models/asset/common.py:93 +#: assets/models/asset/common.py:331 assets/models/cmd_filter.py:36 #: assets/serializers/domain.py:19 assets/serializers/label.py:27 -#: audits/models.py:53 authentication/models/connection_token.py:35 +#: audits/models.py:53 authentication/models/connection_token.py:36 #: perms/models/asset_permission.py:64 perms/serializers/permission.py:34 -#: terminal/backends/command/models.py:20 terminal/models/session/session.py:32 -#: terminal/notifications.py:95 terminal/serializers/command.py:17 +#: terminal/backends/command/models.py:18 terminal/models/session/session.py:31 +#: terminal/notifications.py:155 terminal/serializers/command.py:18 +#: terminal/templates/terminal/_msg_command_warning.html:4 #: tickets/models/ticket/apply_asset.py:16 xpack/plugins/cloud/models.py:212 msgid "Asset" msgstr "资产" -#: accounts/models/account.py:53 accounts/models/account.py:113 +#: accounts/models/account.py:53 accounts/models/account.py:126 #: accounts/serializers/account/account.py:208 #: accounts/serializers/account/account.py:247 #: accounts/serializers/account/template.py:16 @@ -221,10 +230,11 @@ msgstr "来源 ID" #: accounts/models/account.py:61 #: accounts/serializers/automations/change_secret.py:113 #: accounts/serializers/automations/change_secret.py:133 -#: acls/serializers/base.py:119 assets/serializers/asset/common.py:125 +#: acls/serializers/base.py:124 assets/serializers/asset/common.py:125 #: assets/serializers/gateway.py:28 audits/models.py:54 ops/models/base.py:18 #: perms/models/asset_permission.py:70 perms/serializers/permission.py:39 -#: terminal/backends/command/models.py:21 terminal/models/session/session.py:34 +#: terminal/backends/command/models.py:19 terminal/models/session/session.py:33 +#: terminal/templates/terminal/_msg_command_warning.html:8 #: tickets/models/ticket/command_confirm.py:13 xpack/plugins/cloud/models.py:85 msgid "Account" msgstr "账号" @@ -249,15 +259,15 @@ msgstr "可以验证账号" msgid "Can push account" msgstr "可以推送账号" -#: accounts/models/account.py:117 +#: accounts/models/account.py:130 msgid "Account template" msgstr "账号模版" -#: accounts/models/account.py:122 +#: accounts/models/account.py:135 msgid "Can view asset account template secret" msgstr "可以查看资产账号模版密码" -#: accounts/models/account.py:123 +#: accounts/models/account.py:136 msgid "Can change asset account template secret" msgstr "可以更改资产账号模版密码" @@ -275,10 +285,10 @@ msgstr "账号备份计划" #: accounts/models/automations/backup_account.py:83 #: assets/models/automations/base.py:115 audits/models.py:60 -#: ops/models/base.py:55 ops/models/celery.py:63 ops/models/job.py:192 +#: ops/models/base.py:55 ops/models/celery.py:63 ops/models/job.py:194 #: ops/templates/ops/celery_task_log.html:75 #: perms/models/asset_permission.py:72 terminal/models/applet/host.py:137 -#: terminal/models/session/session.py:45 +#: terminal/models/session/session.py:44 #: tickets/models/ticket/apply_application.py:30 #: tickets/models/ticket/apply_asset.py:19 msgid "Date start" @@ -310,7 +320,7 @@ msgstr "原因" #: accounts/models/automations/backup_account.py:99 #: accounts/serializers/automations/change_secret.py:111 #: accounts/serializers/automations/change_secret.py:134 -#: ops/serializers/job.py:56 terminal/serializers/session.py:46 +#: ops/serializers/job.py:56 terminal/serializers/session.py:43 msgid "Is success" msgstr "是否成功" @@ -399,7 +409,7 @@ msgstr "开始日期" #: accounts/models/automations/change_secret.py:91 #: assets/models/automations/base.py:116 ops/models/base.py:56 -#: ops/models/celery.py:64 ops/models/job.py:193 +#: ops/models/celery.py:64 ops/models/job.py:195 #: terminal/models/applet/host.py:138 msgid "Date finished" msgstr "结束日期" @@ -425,7 +435,7 @@ msgstr "最后登录日期" #: accounts/models/automations/gather_account.py:17 #: accounts/models/automations/push_account.py:15 accounts/models/base.py:34 -#: acls/serializers/base.py:18 acls/serializers/base.py:49 +#: acls/serializers/base.py:19 acls/serializers/base.py:50 #: assets/models/_user.py:23 audits/models.py:179 authentication/forms.py:25 #: authentication/forms.py:27 authentication/models/temp_token.py:9 #: authentication/templates/authentication/_msg_different_city.html:9 @@ -457,8 +467,8 @@ msgstr "收集账号" msgid "Triggers" msgstr "触发方式" -#: accounts/models/automations/push_account.py:16 acls/models/base.py:46 -#: acls/serializers/base.py:56 assets/models/cmd_filter.py:81 +#: accounts/models/automations/push_account.py:16 acls/models/base.py:41 +#: acls/serializers/base.py:57 assets/models/cmd_filter.py:81 #: audits/models.py:87 audits/serializers.py:82 #: authentication/serializers/connect_token_secret.py:116 #: authentication/templates/authentication/_access_key_modal.html:34 @@ -473,22 +483,22 @@ msgstr "账号推送" msgid "Verify asset account" msgstr "账号验证" -#: accounts/models/base.py:33 acls/models/base.py:40 acls/models/base.py:101 -#: acls/models/command_acl.py:21 acls/serializers/base.py:34 +#: accounts/models/base.py:33 acls/models/base.py:35 acls/models/base.py:96 +#: acls/models/command_acl.py:21 acls/serializers/base.py:35 #: applications/models.py:9 assets/models/_user.py:22 #: assets/models/asset/common.py:91 assets/models/asset/common.py:149 #: assets/models/cmd_filter.py:21 assets/models/domain.py:18 -#: assets/models/group.py:20 assets/models/label.py:18 -#: assets/models/platform.py:13 assets/models/platform.py:81 -#: assets/serializers/asset/common.py:145 assets/serializers/platform.py:99 -#: assets/serializers/platform.py:199 +#: assets/models/group.py:17 assets/models/label.py:18 +#: assets/models/platform.py:15 assets/models/platform.py:88 +#: assets/serializers/asset/common.py:146 assets/serializers/platform.py:110 +#: assets/serializers/platform.py:223 #: authentication/serializers/connect_token_secret.py:110 ops/mixin.py:21 #: ops/models/adhoc.py:21 ops/models/celery.py:15 ops/models/celery.py:57 -#: ops/models/job.py:92 ops/models/playbook.py:23 ops/serializers/job.py:20 +#: ops/models/job.py:94 ops/models/playbook.py:23 ops/serializers/job.py:20 #: orgs/models.py:80 perms/models/asset_permission.py:56 rbac/models/role.py:29 #: settings/models.py:33 settings/serializers/sms.py:6 #: terminal/models/applet/applet.py:32 terminal/models/component/endpoint.py:12 -#: terminal/models/component/endpoint.py:90 +#: terminal/models/component/endpoint.py:92 #: terminal/models/component/storage.py:26 terminal/models/component/task.py:15 #: terminal/models/component/terminal.py:84 users/forms/profile.py:33 #: users/models/group.py:13 users/models/user.py:753 @@ -504,7 +514,8 @@ msgstr "特权账号" #: assets/models/automations/base.py:21 assets/models/cmd_filter.py:39 #: assets/models/label.py:22 #: authentication/serializers/connect_token_secret.py:114 -#: terminal/models/applet/applet.py:39 users/serializers/user.py:170 +#: terminal/models/applet/applet.py:39 +#: terminal/models/component/endpoint.py:103 users/serializers/user.py:169 msgid "Is active" msgstr "激活" @@ -555,9 +566,9 @@ msgid "Exist policy" msgstr "账号存在策略" #: accounts/serializers/account/account.py:180 applications/models.py:11 -#: assets/models/label.py:21 assets/models/platform.py:82 +#: assets/models/label.py:21 assets/models/platform.py:89 #: assets/serializers/asset/common.py:121 assets/serializers/cagegory.py:8 -#: assets/serializers/platform.py:117 assets/serializers/platform.py:200 +#: assets/serializers/platform.py:128 assets/serializers/platform.py:224 #: perms/serializers/user_permission.py:26 settings/models.py:35 #: tickets/models/ticket/apply_application.py:13 msgid "Category" @@ -567,14 +578,14 @@ msgstr "类别" #: accounts/serializers/automations/base.py:54 acls/models/command_acl.py:24 #: acls/serializers/command_acl.py:18 applications/models.py:14 #: assets/models/_user.py:50 assets/models/automations/base.py:20 -#: assets/models/cmd_filter.py:74 assets/models/platform.py:83 -#: assets/serializers/asset/common.py:122 assets/serializers/platform.py:101 -#: assets/serializers/platform.py:116 audits/serializers.py:48 -#: authentication/serializers/connect_token_secret.py:123 ops/models/job.py:103 +#: assets/models/cmd_filter.py:74 assets/models/platform.py:90 +#: assets/serializers/asset/common.py:122 assets/serializers/platform.py:112 +#: assets/serializers/platform.py:127 audits/serializers.py:48 +#: authentication/serializers/connect_token_secret.py:123 ops/models/job.py:105 #: perms/serializers/user_permission.py:27 terminal/models/applet/applet.py:38 #: terminal/models/component/storage.py:57 #: terminal/models/component/storage.py:146 terminal/serializers/applet.py:29 -#: terminal/serializers/session.py:23 terminal/serializers/storage.py:224 +#: terminal/serializers/session.py:20 terminal/serializers/storage.py:224 #: terminal/serializers/storage.py:236 tickets/models/comment.py:26 #: tickets/models/flow.py:56 tickets/models/ticket/apply_application.py:16 #: tickets/models/ticket/general.py:275 tickets/serializers/flow.py:53 @@ -603,10 +614,10 @@ msgid "Changed" msgstr "已修改" #: accounts/serializers/account/account.py:250 -#: accounts/serializers/automations/base.py:22 acls/models/base.py:102 +#: accounts/serializers/automations/base.py:22 acls/models/base.py:97 #: assets/models/automations/base.py:19 #: assets/serializers/automations/base.py:20 ops/models/base.py:17 -#: ops/models/job.py:105 ops/serializers/job.py:21 +#: ops/models/job.py:107 ops/serializers/job.py:21 #: terminal/templates/terminal/_msg_command_execute_alert.html:16 msgid "Assets" msgstr "资产" @@ -631,26 +642,27 @@ msgstr "账号已存在" msgid "ID" msgstr "ID" -#: accounts/serializers/account/account.py:427 acls/serializers/base.py:111 +#: accounts/serializers/account/account.py:427 acls/serializers/base.py:116 #: assets/models/cmd_filter.py:24 assets/models/label.py:16 audits/models.py:49 #: audits/models.py:85 audits/models.py:163 -#: authentication/models/connection_token.py:31 +#: authentication/models/connection_token.py:32 #: authentication/models/sso_token.py:16 #: notifications/models/notification.py:12 #: perms/api/user_permission/mixin.py:55 perms/models/asset_permission.py:58 -#: perms/serializers/permission.py:30 rbac/builtin.py:122 -#: rbac/models/rolebinding.py:49 terminal/backends/command/models.py:19 -#: terminal/models/session/session.py:30 terminal/models/session/sharing.py:32 -#: terminal/notifications.py:96 terminal/notifications.py:144 -#: terminal/serializers/command.py:16 tickets/models/comment.py:21 -#: users/const.py:14 users/models/user.py:947 users/models/user.py:978 -#: users/serializers/group.py:18 +#: perms/serializers/permission.py:30 rbac/builtin.py:123 +#: rbac/models/rolebinding.py:49 terminal/backends/command/models.py:17 +#: terminal/models/session/session.py:29 terminal/models/session/sharing.py:32 +#: terminal/notifications.py:156 terminal/notifications.py:205 +#: terminal/serializers/command.py:17 +#: terminal/templates/terminal/_msg_command_warning.html:6 +#: tickets/models/comment.py:21 users/const.py:14 users/models/user.py:947 +#: users/models/user.py:978 users/serializers/group.py:18 msgid "User" msgstr "用户" #: accounts/serializers/account/account.py:428 #: authentication/templates/authentication/_access_key_modal.html:33 -#: terminal/notifications.py:98 terminal/notifications.py:146 +#: terminal/notifications.py:158 terminal/notifications.py:207 msgid "Date" msgstr "日期" @@ -680,13 +692,17 @@ msgid "Key password" msgstr "密钥密码" #: accounts/serializers/account/base.py:80 -#: assets/serializers/asset/common.py:306 +#: assets/serializers/asset/common.py:309 msgid "Spec info" msgstr "特殊信息" -#: accounts/serializers/account/base.py:81 -msgid "Tip: If no username is required for authentication, fill in `null`" -msgstr "提示: 如果认证时不需要用户名,可填写为 null" +#: accounts/serializers/account/base.py:82 +msgid "" +"Tip: If no username is required for authentication, fill in `null`, If AD " +"account, like `username@domain`" +msgstr "" +"提示: 如果认证时不需要用户名,可填写为 null, 如果是 AD 账号,格式为 " +"username@domain" #: accounts/serializers/automations/base.py:23 #: assets/models/asset/common.py:155 assets/models/automations/base.py:18 @@ -725,7 +741,7 @@ msgstr "自动化任务执行历史" #: accounts/serializers/automations/change_secret.py:155 audits/const.py:53 #: audits/models.py:59 audits/signal_handlers/activity_log.py:33 #: common/const/choices.py:18 ops/const.py:56 ops/serializers/celery.py:40 -#: terminal/const.py:60 terminal/models/session/sharing.py:107 +#: terminal/const.py:76 terminal/models/session/sharing.py:107 #: tickets/views/approve.py:114 msgid "Success" msgstr "成功" @@ -774,57 +790,62 @@ msgstr "密钥不合法或密钥密码错误" msgid "Acls" msgstr "访问控制" -#: acls/models/base.py:20 tickets/const.py:45 +#: acls/const.py:6 terminal/const.py:11 tickets/const.py:45 #: tickets/templates/tickets/approve_check_password.html:49 msgid "Reject" msgstr "拒绝" -#: acls/models/base.py:21 +#: acls/const.py:7 terminal/const.py:9 msgid "Accept" msgstr "接受" -#: acls/models/base.py:22 +#: acls/const.py:8 msgid "Review" msgstr "审批" -#: acls/models/base.py:42 assets/models/_user.py:51 -#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:93 +#: acls/const.py:9 terminal/const.py:10 +msgid "Warning" +msgstr "告警" + +#: acls/models/base.py:37 assets/models/_user.py:51 +#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:95 msgid "Priority" msgstr "优先级" -#: acls/models/base.py:43 assets/models/_user.py:51 -#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:94 +#: acls/models/base.py:38 assets/models/_user.py:51 +#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:96 msgid "1-100, the lower the value will be match first" msgstr "优先级可选范围为 1-100 (数值越小越优先)" -#: acls/models/base.py:47 assets/models/cmd_filter.py:86 +#: acls/models/base.py:42 assets/models/cmd_filter.py:86 #: authentication/serializers/connect_token_secret.py:88 msgid "Reviewers" msgstr "审批人" -#: acls/models/base.py:48 authentication/models/access_key.py:17 -#: authentication/models/connection_token.py:52 +#: acls/models/base.py:43 authentication/models/access_key.py:17 +#: authentication/models/connection_token.py:53 #: authentication/templates/authentication/_access_key_modal.html:32 #: perms/models/asset_permission.py:76 terminal/models/session/sharing.py:27 #: tickets/const.py:37 msgid "Active" msgstr "激活中" -#: acls/models/base.py:86 users/apps.py:9 +#: acls/models/base.py:81 users/apps.py:9 msgid "Users" msgstr "用户管理" -#: acls/models/base.py:103 assets/models/automations/base.py:17 -#: assets/models/cmd_filter.py:38 assets/serializers/asset/common.py:305 +#: acls/models/base.py:98 assets/models/automations/base.py:17 +#: assets/models/cmd_filter.py:38 assets/serializers/asset/common.py:308 #: rbac/tree.py:35 msgid "Accounts" msgstr "账号管理" #: acls/models/command_acl.py:16 assets/models/cmd_filter.py:60 -#: ops/serializers/job.py:55 terminal/const.py:68 -#: terminal/models/session/session.py:43 terminal/serializers/command.py:18 +#: ops/serializers/job.py:55 terminal/const.py:84 +#: terminal/models/session/session.py:42 terminal/serializers/command.py:19 #: terminal/templates/terminal/_msg_command_alert.html:12 #: terminal/templates/terminal/_msg_command_execute_alert.html:10 +#: terminal/templates/terminal/_msg_command_warning.html:23 msgid "Command" msgstr "命令" @@ -848,6 +869,7 @@ msgstr "忽略大小写" #: acls/models/command_acl.py:33 acls/models/command_acl.py:96 #: acls/serializers/command_acl.py:28 #: authentication/serializers/connect_token_secret.py:85 +#: terminal/templates/terminal/_msg_command_warning.html:14 msgid "Command group" msgstr "命令组" @@ -856,6 +878,7 @@ msgid "The generated regular expression is incorrect: {}" msgstr "生成的正则表达式有误" #: acls/models/command_acl.py:100 +#: terminal/templates/terminal/_msg_command_warning.html:12 msgid "Command acl" msgstr "命令过滤" @@ -892,11 +915,11 @@ msgstr "登录资产访问控制" msgid "Login asset confirm" msgstr "登录资产复核" -#: acls/serializers/base.py:10 acls/serializers/login_acl.py:11 +#: acls/serializers/base.py:11 acls/serializers/login_acl.py:11 msgid "With * indicating a match all. " msgstr "* 表示匹配所有. " -#: acls/serializers/base.py:25 +#: acls/serializers/base.py:26 msgid "" "With * indicating a match all. Such as: 192.168.10.1, 192.168.1.0/24, " "10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 (Domain name " @@ -905,15 +928,19 @@ msgstr "" "* 表示匹配所有。例如: 192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:" "db8:2de::e13, 2001:db8:1a:1110::/64 (支持网域)" -#: acls/serializers/base.py:40 assets/serializers/asset/host.py:19 +#: acls/serializers/base.py:41 assets/serializers/asset/host.py:19 msgid "IP/Host" msgstr "IP/主机" -#: acls/serializers/base.py:98 tickets/serializers/ticket/ticket.py:77 +#: acls/serializers/base.py:91 +msgid "Recipients" +msgstr "接收人" + +#: acls/serializers/base.py:103 tickets/serializers/ticket/ticket.py:77 msgid "The organization `{}` does not exist" msgstr "组织 `{}` 不存在" -#: acls/serializers/base.py:104 +#: acls/serializers/base.py:109 msgid "None of the reviewers belong to Organization `{}`" msgstr "所有复核人都不属于组织 `{}`" @@ -947,7 +974,7 @@ msgid "Applications" msgstr "应用管理" #: applications/models.py:16 xpack/plugins/cloud/models.py:33 -#: xpack/plugins/cloud/serializers/account.py:62 +#: xpack/plugins/cloud/serializers/account.py:63 msgid "Attrs" msgstr "属性" @@ -959,11 +986,11 @@ msgstr "应用程序" msgid "Can match application" msgstr "匹配应用" -#: assets/api/asset/asset.py:149 +#: assets/api/asset/asset.py:157 msgid "Cannot create asset directly, you should create a host or other" msgstr "不能直接创建资产, 你应该创建主机或其他资产" -#: assets/api/domain.py:60 +#: assets/api/domain.py:62 msgid "Number required" msgstr "需要为数字" @@ -992,7 +1019,7 @@ msgid "{} disabled" msgstr "{} 已禁用" #: assets/automations/ping_gateway/manager.py:33 -#: authentication/models/connection_token.py:117 +#: authentication/models/connection_token.py:128 msgid "No account" msgstr "没有账号" @@ -1037,6 +1064,20 @@ msgstr "测试网关" msgid "Gather facts" msgstr "收集资产信息" +#: assets/const/base.py:33 audits/const.py:47 +#: terminal/serializers/applet_host.py:32 +msgid "Disabled" +msgstr "禁用" + +#: assets/const/base.py:34 settings/serializers/basic.py:27 +msgid "Basic" +msgstr "基本" + +#: assets/const/base.py:35 assets/const/protocol.py:193 +#: assets/models/asset/web.py:13 +msgid "Script" +msgstr "脚本" + #: assets/const/category.py:10 assets/models/asset/host.py:8 #: settings/serializers/auth/radius.py:16 settings/serializers/auth/sms.py:67 #: terminal/models/component/endpoint.py:13 terminal/serializers/applet.py:17 @@ -1057,12 +1098,13 @@ msgstr "数据库" msgid "Cloud service" msgstr "云服务" -#: assets/const/category.py:14 assets/models/asset/web.py:16 audits/const.py:34 +#: assets/const/category.py:14 assets/models/asset/gpt.py:11 +#: assets/models/asset/web.py:16 audits/const.py:34 #: terminal/models/applet/applet.py:26 msgid "Web" msgstr "Web" -#: assets/const/category.py:15 common/sdk/sms/endpoint.py:20 +#: assets/const/category.py:16 common/sdk/sms/endpoint.py:20 msgid "Custom type" msgstr "自定义" @@ -1095,32 +1137,84 @@ msgstr "路由器" msgid "Firewall" msgstr "防火墙" +#: assets/const/gpt.py:7 +msgid "ChatGPT" +msgstr "ChatGPT" + #: assets/const/host.py:12 rbac/tree.py:28 msgid "Other" msgstr "其它" -#: assets/const/types.py:223 +#: assets/const/protocol.py:43 +msgid "SFTP enabled" +msgstr "SFTP 已启用" + +#: assets/const/protocol.py:48 +msgid "SFTP home" +msgstr "SFTP 根路径" + +#: assets/const/protocol.py:59 +msgid "Console" +msgstr "控制台" + +#: assets/const/protocol.py:60 +msgid "Connect to console session" +msgstr "连接到控制台会话" + +#: assets/const/protocol.py:64 +msgid "Any" +msgstr "任意" + +#: assets/const/protocol.py:66 settings/serializers/security.py:151 +msgid "Security" +msgstr "安全" + +#: assets/const/protocol.py:67 +msgid "Security layer to use for the connection" +msgstr "连接 RDP 使用的安全层" + +#: assets/const/protocol.py:73 +msgid "AD domain" +msgstr "AD 网域" + +#: assets/const/protocol.py:92 assets/models/asset/database.py:10 +#: settings/serializers/email.py:37 +msgid "Use SSL" +msgstr "使用 SSL" + +#: assets/const/protocol.py:149 +msgid "Auth username" +msgstr "使用用户名认证" + +#: assets/const/protocol.py:170 assets/models/asset/web.py:9 +#: assets/serializers/asset/info/spec.py:16 +msgid "Autofill" +msgstr "自动代填" + +#: assets/const/protocol.py:178 assets/models/asset/web.py:10 +msgid "Username selector" +msgstr "用户名选择器" + +#: assets/const/protocol.py:183 assets/models/asset/web.py:11 +msgid "Password selector" +msgstr "密码选择器" + +#: assets/const/protocol.py:188 assets/models/asset/web.py:12 +msgid "Submit selector" +msgstr "确认按钮选择器" + +#: assets/const/protocol.py:211 +msgid "API mode" +msgstr "API 模式" + +#: assets/const/types.py:224 msgid "All types" msgstr "所有类型" -#: assets/const/web.py:8 +#: assets/const/web.py:7 msgid "Website" msgstr "网站" -#: assets/const/web.py:59 audits/const.py:47 -#: terminal/serializers/applet_host.py:32 -msgid "Disabled" -msgstr "禁用" - -#: assets/const/web.py:60 settings/serializers/basic.py:27 -msgid "Basic" -msgstr "基本" - -#: assets/const/web.py:61 assets/models/asset/web.py:13 -#: assets/serializers/platform.py:40 -msgid "Script" -msgstr "脚本" - #: assets/exceptions.py:12 msgid "This function is not supported temporarily" msgstr "暂时不支持此功能" @@ -1134,21 +1228,21 @@ msgid "SSH public key" msgstr "SSH公钥" #: assets/models/_user.py:27 assets/models/cmd_filter.py:40 -#: assets/models/cmd_filter.py:88 assets/models/group.py:23 -#: common/db/models.py:36 ops/models/adhoc.py:27 ops/models/job.py:111 +#: assets/models/cmd_filter.py:88 assets/models/group.py:20 +#: common/db/models.py:36 ops/models/adhoc.py:27 ops/models/job.py:113 #: ops/models/playbook.py:26 rbac/models/role.py:37 settings/models.py:38 -#: terminal/models/applet/applet.py:44 terminal/models/applet/applet.py:244 +#: terminal/models/applet/applet.py:44 terminal/models/applet/applet.py:248 #: terminal/models/applet/host.py:139 terminal/models/component/endpoint.py:24 -#: terminal/models/component/endpoint.py:100 -#: terminal/models/session/session.py:47 tickets/models/comment.py:32 +#: terminal/models/component/endpoint.py:102 +#: terminal/models/session/session.py:46 tickets/models/comment.py:32 #: tickets/models/ticket/general.py:297 users/models/user.py:792 #: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:111 msgid "Comment" msgstr "备注" #: assets/models/_user.py:28 assets/models/automations/base.py:114 -#: assets/models/cmd_filter.py:41 assets/models/group.py:22 -#: common/db/models.py:34 ops/models/base.py:54 ops/models/job.py:191 +#: assets/models/cmd_filter.py:41 assets/models/group.py:19 +#: common/db/models.py:34 ops/models/base.py:54 ops/models/job.py:193 #: users/models/user.py:979 msgid "Date created" msgstr "创建日期" @@ -1159,7 +1253,7 @@ msgid "Date updated" msgstr "更新日期" #: assets/models/_user.py:30 assets/models/cmd_filter.py:44 -#: assets/models/cmd_filter.py:91 assets/models/group.py:21 +#: assets/models/cmd_filter.py:91 assets/models/group.py:18 #: common/db/models.py:32 users/models/user.py:799 #: users/serializers/group.py:29 msgid "Created by" @@ -1185,10 +1279,10 @@ msgstr "特权用户" msgid "Username same with user" msgstr "用户名与用户相同" -#: assets/models/_user.py:52 authentication/models/connection_token.py:40 +#: assets/models/_user.py:52 authentication/models/connection_token.py:41 #: authentication/serializers/connect_token_secret.py:111 -#: terminal/models/applet/applet.py:41 terminal/serializers/session.py:21 -#: terminal/serializers/session.py:42 terminal/serializers/storage.py:68 +#: terminal/models/applet/applet.py:41 terminal/serializers/session.py:18 +#: terminal/serializers/session.py:39 terminal/serializers/storage.py:68 msgid "Protocol" msgstr "协议" @@ -1240,17 +1334,17 @@ msgstr "可以匹配系统用户" msgid "Cloud" msgstr "云服务" -#: assets/models/asset/common.py:92 assets/models/platform.py:14 +#: assets/models/asset/common.py:92 assets/models/platform.py:16 #: settings/serializers/auth/radius.py:17 settings/serializers/auth/sms.py:68 #: xpack/plugins/cloud/serializers/account_attrs.py:73 msgid "Port" msgstr "端口" -#: assets/models/asset/common.py:150 assets/serializers/asset/common.py:146 +#: assets/models/asset/common.py:150 assets/serializers/asset/common.py:147 msgid "Address" msgstr "地址" -#: assets/models/asset/common.py:151 assets/models/platform.py:112 +#: assets/models/asset/common.py:151 assets/models/platform.py:119 #: authentication/serializers/connect_token_secret.py:115 #: perms/serializers/user_permission.py:24 #: xpack/plugins/cloud/serializers/account_attrs.py:196 @@ -1267,28 +1361,28 @@ msgstr "网域" msgid "Labels" msgstr "标签管理" -#: assets/models/asset/common.py:158 assets/serializers/asset/common.py:307 +#: assets/models/asset/common.py:158 assets/serializers/asset/common.py:310 #: assets/serializers/asset/host.py:11 msgid "Gathered info" msgstr "收集资产硬件信息" #: assets/models/asset/common.py:159 assets/serializers/asset/custom.py:14 msgid "Custom info" -msgstr "自动化信息" +msgstr "自定义属性" -#: assets/models/asset/common.py:335 +#: assets/models/asset/common.py:334 msgid "Can refresh asset hardware info" msgstr "可以更新资产硬件信息" -#: assets/models/asset/common.py:336 +#: assets/models/asset/common.py:335 msgid "Can test asset connectivity" msgstr "可以测试资产连接性" -#: assets/models/asset/common.py:337 +#: assets/models/asset/common.py:336 msgid "Can match asset" msgstr "可以匹配资产" -#: assets/models/asset/common.py:338 +#: assets/models/asset/common.py:337 msgid "Can change asset nodes" msgstr "可以修改资产节点" @@ -1296,11 +1390,6 @@ msgstr "可以修改资产节点" msgid "Custom asset" msgstr "自定义资产" -#: assets/models/asset/database.py:10 assets/serializers/platform.py:45 -#: settings/serializers/email.py:37 -msgid "Use SSL" -msgstr "使用 SSL" - #: assets/models/asset/database.py:11 msgid "CA cert" msgstr "CA 证书" @@ -1317,24 +1406,11 @@ msgstr "客户端密钥" msgid "Allow invalid cert" msgstr "忽略证书校验" -#: assets/models/asset/web.py:9 assets/serializers/asset/info/spec.py:16 -#: assets/serializers/platform.py:30 -msgid "Autofill" -msgstr "自动代填" +#: assets/models/asset/gpt.py:8 +msgid "Proxy" +msgstr "代理" -#: assets/models/asset/web.py:10 assets/serializers/platform.py:32 -msgid "Username selector" -msgstr "用户名选择器" - -#: assets/models/asset/web.py:11 assets/serializers/platform.py:35 -msgid "Password selector" -msgstr "密码选择器" - -#: assets/models/asset/web.py:12 assets/serializers/platform.py:38 -msgid "Submit selector" -msgstr "确认按钮选择器" - -#: assets/models/automations/base.py:22 ops/models/job.py:187 +#: assets/models/automations/base.py:22 ops/models/job.py:189 #: settings/serializers/auth/sms.py:99 msgid "Parameters" msgstr "参数" @@ -1348,8 +1424,8 @@ msgid "Asset automation task" msgstr "资产自动化任务" #: assets/models/automations/base.py:113 audits/models.py:199 -#: audits/serializers.py:49 ops/models/base.py:49 ops/models/job.py:184 -#: terminal/models/applet/applet.py:243 terminal/models/applet/host.py:136 +#: audits/serializers.py:49 ops/models/base.py:49 ops/models/job.py:186 +#: terminal/models/applet/applet.py:247 terminal/models/applet/host.py:136 #: terminal/models/component/status.py:30 terminal/serializers/applet.py:18 #: terminal/serializers/applet_host.py:107 tickets/models/ticket/general.py:283 #: tickets/serializers/super_ticket.py:13 @@ -1412,17 +1488,17 @@ msgstr "收藏的资产" msgid "Gateway" msgstr "网关" -#: assets/models/group.py:30 +#: assets/models/group.py:27 msgid "Asset group" msgstr "资产组" -#: assets/models/group.py:34 assets/models/platform.py:17 -#: assets/serializers/platform.py:102 +#: assets/models/group.py:31 assets/models/platform.py:19 +#: assets/serializers/platform.py:113 #: xpack/plugins/cloud/providers/nutanix.py:30 msgid "Default" msgstr "默认" -#: assets/models/group.py:34 +#: assets/models/group.py:31 msgid "Default asset group" msgstr "默认资产组" @@ -1430,9 +1506,9 @@ msgstr "默认资产组" msgid "System" msgstr "系统" -#: assets/models/label.py:19 assets/models/node.py:557 +#: assets/models/label.py:19 assets/models/node.py:545 #: assets/serializers/cagegory.py:7 assets/serializers/cagegory.py:14 -#: authentication/models/connection_token.py:28 +#: authentication/models/connection_token.py:29 #: authentication/serializers/connect_token_secret.py:122 #: common/serializers/common.py:86 settings/models.py:34 msgid "Value" @@ -1440,7 +1516,7 @@ msgstr "值" #: assets/models/label.py:40 assets/serializers/asset/common.py:123 #: assets/serializers/cagegory.py:6 assets/serializers/cagegory.py:13 -#: assets/serializers/platform.py:100 +#: assets/serializers/platform.py:111 #: authentication/serializers/connect_token_secret.py:121 #: common/serializers/common.py:85 perms/serializers/user_permission.py:28 #: settings/serializers/sms.py:7 @@ -1451,144 +1527,145 @@ msgstr "标签" msgid "New node" msgstr "新节点" -#: assets/models/node.py:485 audits/backends/db.py:55 audits/backends/db.py:56 +#: assets/models/node.py:473 audits/backends/db.py:55 audits/backends/db.py:56 msgid "empty" msgstr "空" -#: assets/models/node.py:556 perms/models/perm_node.py:28 +#: assets/models/node.py:544 perms/models/perm_node.py:28 msgid "Key" msgstr "键" -#: assets/models/node.py:558 assets/serializers/node.py:20 +#: assets/models/node.py:546 assets/serializers/node.py:20 msgid "Full value" msgstr "全称" -#: assets/models/node.py:562 perms/models/perm_node.py:30 +#: assets/models/node.py:550 perms/models/perm_node.py:30 msgid "Parent key" msgstr "ssh私钥" -#: assets/models/node.py:571 perms/serializers/permission.py:35 +#: assets/models/node.py:559 perms/serializers/permission.py:35 #: tickets/models/ticket/apply_asset.py:14 xpack/plugins/cloud/models.py:96 msgid "Node" msgstr "节点" -#: assets/models/node.py:574 +#: assets/models/node.py:562 msgid "Can match node" msgstr "可以匹配节点" -#: assets/models/platform.py:15 +#: assets/models/platform.py:17 msgid "Primary" msgstr "主要的" -#: assets/models/platform.py:16 +#: assets/models/platform.py:18 msgid "Required" msgstr "必须的" -#: assets/models/platform.py:18 +#: assets/models/platform.py:20 msgid "Public" msgstr "开放的" -#: assets/models/platform.py:19 settings/serializers/settings.py:67 +#: assets/models/platform.py:21 assets/serializers/platform.py:48 +#: settings/serializers/settings.py:67 #: users/templates/users/reset_password.html:29 msgid "Setting" msgstr "设置" -#: assets/models/platform.py:31 audits/const.py:48 settings/models.py:37 +#: assets/models/platform.py:38 audits/const.py:48 settings/models.py:37 #: terminal/serializers/applet_host.py:33 msgid "Enabled" msgstr "启用" -#: assets/models/platform.py:32 +#: assets/models/platform.py:39 msgid "Ansible config" msgstr "Ansible 配置" -#: assets/models/platform.py:34 assets/serializers/platform.py:63 +#: assets/models/platform.py:41 assets/serializers/platform.py:32 msgid "Ping enabled" msgstr "启用资产探活" -#: assets/models/platform.py:35 assets/serializers/platform.py:64 +#: assets/models/platform.py:42 assets/serializers/platform.py:33 msgid "Ping method" msgstr "资产探活方式" -#: assets/models/platform.py:36 +#: assets/models/platform.py:43 msgid "Ping params" msgstr "资产探活参数" -#: assets/models/platform.py:38 assets/models/platform.py:62 -#: assets/serializers/platform.py:65 +#: assets/models/platform.py:45 assets/models/platform.py:69 +#: assets/serializers/platform.py:34 msgid "Gather facts enabled" msgstr "启用收集资产信息" -#: assets/models/platform.py:40 assets/models/platform.py:64 -#: assets/serializers/platform.py:66 +#: assets/models/platform.py:47 assets/models/platform.py:71 +#: assets/serializers/platform.py:35 msgid "Gather facts method" msgstr "收集信息方式" -#: assets/models/platform.py:42 assets/models/platform.py:66 +#: assets/models/platform.py:49 assets/models/platform.py:73 msgid "Gather facts params" msgstr "收集信息参数" -#: assets/models/platform.py:44 assets/serializers/platform.py:69 +#: assets/models/platform.py:51 assets/serializers/platform.py:38 msgid "Change secret enabled" msgstr "启用改密" -#: assets/models/platform.py:46 assets/serializers/platform.py:70 +#: assets/models/platform.py:53 assets/serializers/platform.py:39 msgid "Change secret method" msgstr "改密方式" -#: assets/models/platform.py:48 +#: assets/models/platform.py:55 msgid "Change secret params" msgstr "改密参数" -#: assets/models/platform.py:50 assets/serializers/platform.py:71 +#: assets/models/platform.py:57 assets/serializers/platform.py:40 msgid "Push account enabled" msgstr "启用账号推送" -#: assets/models/platform.py:52 assets/serializers/platform.py:72 +#: assets/models/platform.py:59 assets/serializers/platform.py:41 msgid "Push account method" msgstr "账号推送方式" -#: assets/models/platform.py:54 +#: assets/models/platform.py:61 msgid "Push account params" msgstr "账号推送参数" -#: assets/models/platform.py:56 assets/serializers/platform.py:67 +#: assets/models/platform.py:63 assets/serializers/platform.py:36 msgid "Verify account enabled" msgstr "开启账号验证" -#: assets/models/platform.py:58 assets/serializers/platform.py:68 +#: assets/models/platform.py:65 assets/serializers/platform.py:37 msgid "Verify account method" msgstr "账号验证方式" -#: assets/models/platform.py:60 +#: assets/models/platform.py:67 msgid "Verify account params" msgstr "账号验证参数" -#: assets/models/platform.py:84 tickets/models/ticket/general.py:300 +#: assets/models/platform.py:91 tickets/models/ticket/general.py:300 msgid "Meta" msgstr "元数据" -#: assets/models/platform.py:85 +#: assets/models/platform.py:92 msgid "Internal" msgstr "内置" -#: assets/models/platform.py:89 assets/serializers/platform.py:115 +#: assets/models/platform.py:96 assets/serializers/platform.py:126 msgid "Charset" msgstr "编码" -#: assets/models/platform.py:91 assets/serializers/platform.py:143 +#: assets/models/platform.py:98 assets/serializers/platform.py:154 msgid "Domain enabled" msgstr "启用网域" -#: assets/models/platform.py:93 assets/serializers/platform.py:142 +#: assets/models/platform.py:100 assets/serializers/platform.py:153 msgid "Su enabled" msgstr "启用账号切换" -#: assets/models/platform.py:94 assets/serializers/platform.py:121 +#: assets/models/platform.py:101 assets/serializers/platform.py:132 msgid "Su method" msgstr "账号切换方式" -#: assets/models/platform.py:95 assets/serializers/platform.py:124 +#: assets/models/platform.py:102 assets/serializers/platform.py:135 msgid "Custom fields" msgstr "自定义属性" @@ -1603,7 +1680,7 @@ msgid "" "type" msgstr "资产中批量更新平台,不符合平台类型跳过的资产" -#: assets/serializers/asset/common.py:124 assets/serializers/platform.py:118 +#: assets/serializers/asset/common.py:124 assets/serializers/platform.py:129 #: authentication/serializers/connect_token_secret.py:29 #: authentication/serializers/connect_token_secret.py:72 #: perms/serializers/user_permission.py:25 xpack/plugins/cloud/models.py:99 @@ -1611,24 +1688,24 @@ msgid "Protocols" msgstr "协议组" #: assets/serializers/asset/common.py:126 -#: assets/serializers/asset/common.py:147 +#: assets/serializers/asset/common.py:148 msgid "Node path" msgstr "节点路径" -#: assets/serializers/asset/common.py:144 -#: assets/serializers/asset/common.py:308 +#: assets/serializers/asset/common.py:145 +#: assets/serializers/asset/common.py:311 msgid "Auto info" msgstr "自动化信息" -#: assets/serializers/asset/common.py:227 +#: assets/serializers/asset/common.py:234 msgid "Platform not exist" msgstr "平台不存在" -#: assets/serializers/asset/common.py:263 +#: assets/serializers/asset/common.py:270 msgid "port out of range (0-65535)" msgstr "端口超出范围 (0-65535)" -#: assets/serializers/asset/common.py:270 +#: assets/serializers/asset/common.py:277 msgid "Protocol is required: {}" msgstr "协议是必填的: {}" @@ -1636,8 +1713,8 @@ msgstr "协议是必填的: {}" msgid "Default database" msgstr "默认数据库" -#: assets/serializers/asset/database.py:28 common/db/fields.py:570 -#: common/db/fields.py:575 common/serializers/fields.py:104 +#: assets/serializers/asset/database.py:28 common/db/fields.py:579 +#: common/db/fields.py:584 common/serializers/fields.py:104 #: tickets/serializers/ticket/common.py:58 #: xpack/plugins/cloud/serializers/account_attrs.py:56 #: xpack/plugins/cloud/serializers/account_attrs.py:79 @@ -1645,6 +1722,22 @@ msgstr "默认数据库" msgid "This field is required." msgstr "该字段是必填项。" +#: assets/serializers/asset/gpt.py:20 +msgid "" +"If the server cannot directly connect to the API address, you need set up an " +"HTTP proxy. e.g. http(s)://host:port" +msgstr "" +"如果服务器不能直接访问 api 地址,你需要设置一个 HTTP 代理。例如 http(s)://" +"host:port" + +#: assets/serializers/asset/gpt.py:24 +msgid "HTTP proxy" +msgstr "HTTP(s) 代理" + +#: assets/serializers/asset/gpt.py:31 +msgid "Proxy must start with http:// or https://" +msgstr "代理必须以 http:// 或 https:// 开头" + #: assets/serializers/asset/info/gathered.py:6 msgid "Vendor" msgstr "制造商" @@ -1719,47 +1812,59 @@ msgstr "值" msgid "Can't contains: /" msgstr "不能包含: /" -#: assets/serializers/platform.py:26 -msgid "SFTP enabled" -msgstr "SFTP 已启用" - -#: assets/serializers/platform.py:27 -msgid "SFTP home" -msgstr "SFTP 根路径" - #: assets/serializers/platform.py:42 -msgid "Auth with username" -msgstr "使用用户名认证" - -#: assets/serializers/platform.py:73 msgid "Gather accounts enabled" msgstr "启用账号收集" -#: assets/serializers/platform.py:74 +#: assets/serializers/platform.py:43 msgid "Gather accounts method" msgstr "收集账号方式" -#: assets/serializers/platform.py:103 +#: assets/serializers/platform.py:49 +msgid "Port from addr" +msgstr "端口来自地址" + +#: assets/serializers/platform.py:61 +msgid "" +"This protocol is primary, and it must be set when adding assets. " +"Additionally, there can only be one primary protocol." +msgstr "该协议是主要的,添加资产时必须设置。并且只能有一个主要协议" + +#: assets/serializers/platform.py:66 +msgid "This protocol is required, and it must be set when adding assets." +msgstr "该协议是必填的,添加资产时必须设置" + +#: assets/serializers/platform.py:69 +msgid "" +"This protocol is default, when adding assets, it will be displayed by " +"default." +msgstr "该协议是默认的,添加资产时,将默认显示" + +#: assets/serializers/platform.py:72 +msgid "This protocol is public, asset will show this protocol to user" +msgstr "该协议是公开的,资产将向用户显示该协议并可以连接使用" + +#: assets/serializers/platform.py:114 msgid "Help text" msgstr "帮助" -#: assets/serializers/platform.py:104 +#: assets/serializers/platform.py:115 msgid "Choices" msgstr "选择" -#: assets/serializers/platform.py:119 +#: assets/serializers/platform.py:130 msgid "Automation" msgstr "自动化" -#: assets/serializers/platform.py:144 +#: assets/serializers/platform.py:155 msgid "Default Domain" msgstr "默认网域" -#: assets/serializers/platform.py:153 +#: assets/serializers/platform.py:176 msgid "type is required" msgstr "类型 该字段是必填项。" -#: assets/serializers/platform.py:176 +#: assets/serializers/platform.py:199 msgid "Protocols is required" msgstr "协议是必填的" @@ -1851,7 +1956,7 @@ msgstr "删除目录" #: audits/const.py:14 audits/const.py:25 #: authentication/templates/authentication/_access_key_modal.html:65 -#: perms/const.py:17 rbac/tree.py:230 +#: perms/const.py:17 rbac/tree.py:231 msgid "Delete" msgstr "删除" @@ -1875,13 +1980,14 @@ msgstr "下载" msgid "Rename dir" msgstr "映射目录" -#: audits/const.py:23 rbac/tree.py:228 +#: audits/const.py:23 rbac/tree.py:229 +#: terminal/templates/terminal/_msg_command_warning.html:18 msgid "View" msgstr "查看" #: audits/const.py:26 #: authentication/templates/authentication/_access_key_modal.html:22 -#: rbac/tree.py:227 +#: rbac/tree.py:228 msgid "Create" msgstr "创建" @@ -1901,7 +2007,7 @@ msgstr "改密" #: audits/const.py:35 settings/serializers/terminal.py:6 #: terminal/models/applet/host.py:25 terminal/models/component/terminal.py:163 -#: terminal/serializers/session.py:49 terminal/serializers/session.py:58 +#: terminal/serializers/session.py:46 terminal/serializers/session.py:55 msgid "Terminal" msgstr "终端" @@ -1939,7 +2045,7 @@ msgid "Job audit log" msgstr "作业审计日志" #: audits/models.py:51 audits/models.py:95 audits/models.py:166 -#: terminal/models/session/session.py:39 terminal/models/session/sharing.py:99 +#: terminal/models/session/session.py:38 terminal/models/session/sharing.py:99 msgid "Remote addr" msgstr "远端地址" @@ -1955,10 +2061,11 @@ msgstr "文件名" msgid "File" msgstr "文件" -#: audits/models.py:62 terminal/backends/command/models.py:24 +#: audits/models.py:62 terminal/backends/command/models.py:22 #: terminal/models/session/replay.py:9 terminal/models/session/sharing.py:18 #: terminal/models/session/sharing.py:81 #: terminal/templates/terminal/_msg_command_alert.html:10 +#: terminal/templates/terminal/_msg_command_warning.html:17 #: tickets/models/ticket/command_confirm.py:15 msgid "Session" msgstr "会话" @@ -1977,7 +2084,7 @@ msgid "Resource" msgstr "资源" #: audits/models.py:96 audits/models.py:142 audits/models.py:168 -#: terminal/serializers/command.py:50 +#: terminal/serializers/command.py:76 msgid "Datetime" msgstr "日期" @@ -2079,14 +2186,14 @@ msgid "Auth Token" msgstr "认证令牌" #: audits/signal_handlers/login_log.py:31 authentication/notifications.py:73 -#: authentication/views/login.py:74 authentication/views/wecom.py:159 +#: authentication/views/login.py:75 authentication/views/wecom.py:159 #: notifications/backends/__init__.py:11 settings/serializers/auth/wecom.py:10 #: users/models/user.py:706 users/models/user.py:814 msgid "WeCom" msgstr "企业微信" #: audits/signal_handlers/login_log.py:32 authentication/views/feishu.py:123 -#: authentication/views/login.py:86 notifications/backends/__init__.py:14 +#: authentication/views/login.py:87 notifications/backends/__init__.py:14 #: settings/serializers/auth/feishu.py:10 #: settings/serializers/auth/feishu.py:13 users/models/user.py:708 #: users/models/user.py:816 @@ -2094,7 +2201,7 @@ msgid "FeiShu" msgstr "飞书" #: audits/signal_handlers/login_log.py:33 authentication/views/dingtalk.py:160 -#: authentication/views/login.py:80 notifications/backends/__init__.py:12 +#: authentication/views/login.py:81 notifications/backends/__init__.py:12 #: settings/serializers/auth/dingtalk.py:10 users/models/user.py:707 #: users/models/user.py:815 msgid "DingTalk" @@ -2117,19 +2224,27 @@ msgstr "上传 FTP 文件到外部存储" msgid "This action require verify your MFA" msgstr "该操作需要验证您的 MFA, 请先开启并配置" -#: authentication/api/connection_token.py:305 +#: authentication/api/connection_token.py:221 +msgid "Reusable connection token is not allowed, global setting not enabled" +msgstr "不允许使用可重复使用的连接令牌,未启用全局设置" + +#: authentication/api/connection_token.py:301 +msgid "Anonymous account is not supported for this asset" +msgstr "匿名账号不支持当前资产" + +#: authentication/api/connection_token.py:323 msgid "Account not found" msgstr "账号未找到" -#: authentication/api/connection_token.py:308 +#: authentication/api/connection_token.py:326 msgid "Permission expired" msgstr "授权已过期" -#: authentication/api/connection_token.py:322 +#: authentication/api/connection_token.py:340 msgid "ACL action is reject: {}({})" msgstr "ACL 动作是拒绝: {}({})" -#: authentication/api/connection_token.py:326 +#: authentication/api/connection_token.py:344 msgid "ACL action is review" msgstr "ACL 动作是复核" @@ -2137,22 +2252,22 @@ msgstr "ACL 动作是复核" msgid "Current user not support mfa type: {}" msgstr "当前用户不支持 MFA 类型: {}" -#: authentication/api/password.py:31 terminal/api/session/session.py:259 +#: authentication/api/password.py:32 terminal/api/session/session.py:259 #: users/views/profile/reset.py:44 msgid "User does not exist: {}" msgstr "用户不存在: {}" -#: authentication/api/password.py:31 users/views/profile/reset.py:127 +#: authentication/api/password.py:32 users/views/profile/reset.py:127 msgid "No user matched" msgstr "没有匹配到用户" -#: authentication/api/password.py:35 +#: authentication/api/password.py:36 msgid "" "The user is from {}, please go to the corresponding system to change the " "password" msgstr "用户来自 {} 请去相应系统修改密码" -#: authentication/api/password.py:59 +#: authentication/api/password.py:60 #: authentication/templates/authentication/login.html:305 #: users/templates/users/forgot_password.html:27 #: users/templates/users/forgot_password.html:28 @@ -2478,7 +2593,7 @@ msgstr "设置手机号码启用" msgid "Clear phone number to disable" msgstr "清空手机号码禁用" -#: authentication/middleware.py:93 settings/utils/ldap.py:657 +#: authentication/middleware.py:93 settings/utils/ldap.py:661 msgid "Authentication failed (before login check failed): {}" msgstr "认证失败(登录前检查失败): {}" @@ -2496,78 +2611,86 @@ msgstr "该 MFA ({}) 方式没有启用" msgid "Please change your password" msgstr "请修改密码" -#: authentication/models/connection_token.py:37 +#: authentication/models/connection_token.py:38 #: terminal/serializers/storage.py:111 msgid "Account name" msgstr "账号名称" -#: authentication/models/connection_token.py:38 +#: authentication/models/connection_token.py:39 msgid "Input username" msgstr "自定义用户名" -#: authentication/models/connection_token.py:39 -#: authentication/serializers/connection_token.py:20 +#: authentication/models/connection_token.py:40 +#: authentication/serializers/connection_token.py:18 msgid "Input secret" msgstr "自定义密码" -#: authentication/models/connection_token.py:41 +#: authentication/models/connection_token.py:42 msgid "Connect method" msgstr "连接方式" -#: authentication/models/connection_token.py:42 +#: authentication/models/connection_token.py:43 msgid "Connect options" msgstr "连接项" -#: authentication/models/connection_token.py:43 +#: authentication/models/connection_token.py:44 #: rbac/serializers/rolebinding.py:21 msgid "User display" msgstr "用户名称" -#: authentication/models/connection_token.py:44 +#: authentication/models/connection_token.py:45 msgid "Asset display" msgstr "资产名称" -#: authentication/models/connection_token.py:45 +#: authentication/models/connection_token.py:46 msgid "Reusable" msgstr "可以重复使用" -#: authentication/models/connection_token.py:46 +#: authentication/models/connection_token.py:47 #: authentication/models/temp_token.py:13 perms/models/asset_permission.py:74 #: tickets/models/ticket/apply_application.py:31 #: tickets/models/ticket/apply_asset.py:20 users/models/user.py:797 msgid "Date expired" msgstr "失效日期" -#: authentication/models/connection_token.py:50 +#: authentication/models/connection_token.py:51 #: perms/models/asset_permission.py:77 msgid "From ticket" msgstr "来自工单" -#: authentication/models/connection_token.py:56 +#: authentication/models/connection_token.py:58 +msgid "Can expire connection token" +msgstr "可以失效连接令牌" + +#: authentication/models/connection_token.py:59 +msgid "Can reuse connection token" +msgstr "可以复用连接令牌" + +#: authentication/models/connection_token.py:61 msgid "Connection token" msgstr "连接令牌" -#: authentication/models/connection_token.py:58 -msgid "Can view connection token secret" -msgstr "可以查看连接令牌密文" - -#: authentication/models/connection_token.py:105 +#: authentication/models/connection_token.py:116 msgid "Connection token inactive" msgstr "连接令牌未激活" -#: authentication/models/connection_token.py:108 +#: authentication/models/connection_token.py:119 msgid "Connection token expired at: {}" msgstr "连接令牌过期: {}" -#: authentication/models/connection_token.py:111 +#: authentication/models/connection_token.py:122 msgid "No user or invalid user" msgstr "没有用户或用户失效" -#: authentication/models/connection_token.py:114 +#: authentication/models/connection_token.py:125 msgid "No asset or inactive asset" msgstr "没有资产或资产未激活" -#: authentication/models/connection_token.py:267 +#: authentication/models/connection_token.py:269 +msgid "Can view super connection token secret" +msgstr "可以查看超级连接令牌密文" + +#: authentication/models/connection_token.py:271 msgid "Super connection token" msgstr "超级连接令牌" @@ -2611,15 +2734,15 @@ msgstr "组件" msgid "Expired now" msgstr "立刻过期" -#: authentication/serializers/connection_token.py:18 +#: authentication/serializers/connection_token.py:16 msgid "Expired time" msgstr "过期时间" -#: authentication/serializers/connection_token.py:22 +#: authentication/serializers/connection_token.py:20 msgid "Ticket info" msgstr "工单信息" -#: authentication/serializers/connection_token.py:23 +#: authentication/serializers/connection_token.py:21 #: perms/models/asset_permission.py:71 perms/serializers/permission.py:36 #: perms/serializers/permission.py:56 #: tickets/models/ticket/apply_application.py:28 @@ -2627,16 +2750,12 @@ msgstr "工单信息" msgid "Actions" msgstr "动作" -#: authentication/serializers/connection_token.py:44 +#: authentication/serializers/connection_token.py:42 #: perms/serializers/permission.py:38 perms/serializers/permission.py:57 -#: users/serializers/user.py:97 users/serializers/user.py:173 +#: users/serializers/user.py:96 users/serializers/user.py:172 msgid "Is expired" msgstr "已过期" -#: authentication/serializers/connection_token.py:79 -msgid "Reusable connection token is not allowed, global setting not enabled" -msgstr "不允许使用可重复使用的连接令牌,未启用全局设置" - #: authentication/serializers/password_mfa.py:16 #: authentication/serializers/password_mfa.py:24 #: notifications/backends/__init__.py:10 settings/serializers/email.py:19 @@ -2653,8 +2772,8 @@ msgid "The {} cannot be empty" msgstr "{} 不能为空" #: authentication/serializers/token.py:79 perms/serializers/permission.py:37 -#: perms/serializers/permission.py:58 users/serializers/user.py:98 -#: users/serializers/user.py:171 +#: perms/serializers/permission.py:58 users/serializers/user.py:97 +#: users/serializers/user.py:170 msgid "Is valid" msgstr "是否有效" @@ -2731,7 +2850,7 @@ msgstr "代码错误" #: authentication/templates/authentication/_msg_reset_password_code.html:9 #: authentication/templates/authentication/_msg_rest_password_success.html:2 #: authentication/templates/authentication/_msg_rest_public_key_success.html:2 -#: jumpserver/conf.py:426 +#: jumpserver/conf.py:431 #: perms/templates/perms/_msg_item_permissions_expire.html:3 #: perms/templates/perms/_msg_permed_items_expire.html:3 #: tickets/templates/tickets/approve_check_password.html:33 @@ -2871,7 +2990,7 @@ msgid "Copy success" msgstr "复制成功" #: authentication/utils.py:28 common/utils/ip/geoip/utils.py:24 -#: xpack/plugins/cloud/const.py:27 +#: xpack/plugins/cloud/const.py:29 msgid "LAN" msgstr "局域网" @@ -2946,23 +3065,23 @@ msgstr "绑定 飞书 成功" msgid "Failed to get user from FeiShu" msgstr "从飞书获取用户失败" -#: authentication/views/login.py:182 +#: authentication/views/login.py:183 msgid "Redirecting" msgstr "跳转中" -#: authentication/views/login.py:183 +#: authentication/views/login.py:184 msgid "Redirecting to {} authentication" msgstr "正在跳转到 {} 认证" -#: authentication/views/login.py:206 -msgid "Please enable cookies and try again." -msgstr "设置你的浏览器支持cookie" +#: authentication/views/login.py:207 +msgid "Login timeout, please try again." +msgstr "登录超时,请重新登录" -#: authentication/views/login.py:247 +#: authentication/views/login.py:250 msgid "User email already exists ({})" msgstr "用户邮箱已存在 ({})" -#: authentication/views/login.py:325 +#: authentication/views/login.py:328 msgid "" "Wait for {} confirm, You also can copy link to her/him
\n" " Don't close this page" @@ -2970,15 +3089,15 @@ msgstr "" "等待 {} 确认, 你也可以复制链接发给他/她
\n" " 不要关闭本页面" -#: authentication/views/login.py:330 +#: authentication/views/login.py:333 msgid "No ticket found" msgstr "没有发现工单" -#: authentication/views/login.py:366 +#: authentication/views/login.py:369 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:367 +#: authentication/views/login.py:370 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" @@ -3026,7 +3145,7 @@ msgstr "定时触发" msgid "Ready" msgstr "准备" -#: common/const/choices.py:16 terminal/const.py:59 tickets/const.py:29 +#: common/const/choices.py:16 terminal/const.py:75 tickets/const.py:29 #: tickets/const.py:39 msgid "Pending" msgstr "待定的" @@ -3081,7 +3200,7 @@ msgstr "编码数据为 text" msgid "Encrypt field using Secret Key" msgstr "加密的字段" -#: common/db/fields.py:558 +#: common/db/fields.py:567 msgid "" "Invalid JSON data for JSONManyToManyField, should be like {'type': 'all'} or " "{'type': 'ids', 'ids': []} or {'type': 'attrs', 'attrs': [{'name': 'ip', " @@ -3091,19 +3210,19 @@ msgstr "" "{'type': 'attrs', 'attrs': [{'name': 'ip', 'match': 'exact', 'value': " "'1.1.1.1'}}" -#: common/db/fields.py:565 +#: common/db/fields.py:574 msgid "Invalid type, should be \"all\", \"ids\" or \"attrs\"" msgstr "无效类型,应为 all、ids 或 attrs" -#: common/db/fields.py:568 +#: common/db/fields.py:577 msgid "Invalid ids for ids, should be a list" msgstr "无效的ID,应为列表" -#: common/db/fields.py:573 common/db/fields.py:578 +#: common/db/fields.py:582 common/db/fields.py:587 msgid "Invalid attrs, should be a list of dict" msgstr "无效的属性,应为dict列表" -#: common/db/fields.py:580 +#: common/db/fields.py:589 msgid "Invalid attrs, should be has name and value" msgstr "无效属性,应具有名称和值" @@ -3135,7 +3254,7 @@ msgstr "组织 ID" msgid "The file content overflowed (The maximum length `{}` bytes)" msgstr "文件内容太大 (最大长度 `{}` 字节)" -#: common/drf/parsers/base.py:193 +#: common/drf/parsers/base.py:195 msgid "Parse file error: {}" msgstr "解析文件错误: {}" @@ -3315,11 +3434,11 @@ msgstr "导出搜素: %s" msgid "User %s view/export secret" msgstr "用户 %s 查看/导出 了密码" -#: jumpserver/conf.py:425 +#: jumpserver/conf.py:430 msgid "Create account successfully" msgstr "创建账号成功" -#: jumpserver/conf.py:427 +#: jumpserver/conf.py:432 msgid "Your account has been created successfully" msgstr "你的账号已创建成功" @@ -3406,11 +3525,11 @@ msgstr "任务 {} 不存在" msgid "Task {} args or kwargs error" msgstr "任务 {} 执行参数错误" -#: ops/api/playbook.py:38 +#: ops/api/playbook.py:37 msgid "Currently playbook is being used in a job" msgstr "当前 playbook 正在作业中使用" -#: ops/api/playbook.py:92 +#: ops/api/playbook.py:91 msgid "Unsupported file content" msgstr "不支持的文件内容" @@ -3454,7 +3573,7 @@ msgstr "VCS" msgid "Adhoc" msgstr "命令" -#: ops/const.py:39 ops/models/job.py:101 +#: ops/const.py:39 ops/models/job.py:103 msgid "Playbook" msgstr "Playbook" @@ -3511,17 +3630,17 @@ msgstr "需要周期或定期设置" msgid "Pattern" msgstr "模式" -#: ops/models/adhoc.py:24 ops/models/job.py:96 +#: ops/models/adhoc.py:24 ops/models/job.py:98 msgid "Module" msgstr "模块" -#: ops/models/adhoc.py:25 ops/models/celery.py:58 ops/models/job.py:95 +#: ops/models/adhoc.py:25 ops/models/celery.py:58 ops/models/job.py:97 #: terminal/models/component/task.py:16 msgid "Args" msgstr "参数" #: ops/models/adhoc.py:26 ops/models/base.py:16 ops/models/base.py:53 -#: ops/models/job.py:104 ops/models/job.py:190 ops/models/playbook.py:25 +#: ops/models/job.py:106 ops/models/job.py:192 ops/models/playbook.py:25 #: terminal/models/session/sharing.py:23 msgid "Creator" msgstr "创建者" @@ -3538,12 +3657,12 @@ msgstr "最后执行" msgid "Date last run" msgstr "最后运行日期" -#: ops/models/base.py:51 ops/models/job.py:188 +#: ops/models/base.py:51 ops/models/job.py:190 #: xpack/plugins/cloud/models.py:162 msgid "Result" msgstr "结果" -#: ops/models/base.py:52 ops/models/job.py:189 +#: ops/models/base.py:52 ops/models/job.py:191 msgid "Summary" msgstr "汇总" @@ -3576,43 +3695,43 @@ msgstr "发布日期" msgid "Celery Task Execution" msgstr "Celery 任务执行" -#: ops/models/job.py:98 +#: ops/models/job.py:100 msgid "Chdir" msgstr "运行目录" -#: ops/models/job.py:99 +#: ops/models/job.py:101 msgid "Timeout (Seconds)" msgstr "超时时间(秒)" -#: ops/models/job.py:106 +#: ops/models/job.py:108 msgid "Use Parameter Define" msgstr "使用参数定义" -#: ops/models/job.py:107 +#: ops/models/job.py:109 msgid "Parameters define" msgstr "参数定义" -#: ops/models/job.py:108 +#: ops/models/job.py:110 msgid "Runas" msgstr "运行用户" -#: ops/models/job.py:110 +#: ops/models/job.py:112 msgid "Runas policy" msgstr "用户策略" -#: ops/models/job.py:172 +#: ops/models/job.py:174 msgid "Job" msgstr "作业" -#: ops/models/job.py:195 +#: ops/models/job.py:197 msgid "Material" msgstr "Material" -#: ops/models/job.py:197 +#: ops/models/job.py:199 msgid "Material Type" msgstr "Material 类型" -#: ops/models/job.py:460 +#: ops/models/job.py:480 msgid "Job Execution" msgstr "作业执行" @@ -3660,7 +3779,7 @@ msgstr "保存后执行" msgid "Job type" msgstr "任务类型" -#: ops/serializers/job.py:57 terminal/serializers/session.py:50 +#: ops/serializers/job.py:57 terminal/serializers/session.py:47 msgid "Is finished" msgstr "是否完成" @@ -3750,13 +3869,14 @@ msgstr "LDAP 同步设置组织为当前组织,请切换其他组织后再进 msgid "The organization have resource ({}) cannot be deleted" msgstr "组织存在资源 ({}) 不能被删除" -#: orgs/apps.py:7 rbac/tree.py:118 +#: orgs/apps.py:7 rbac/tree.py:119 msgid "App organizations" msgstr "组织管理" #: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:89 #: rbac/const.py:7 rbac/models/rolebinding.py:56 #: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:63 +#: terminal/templates/terminal/_msg_command_warning.html:21 #: tickets/models/ticket/general.py:302 tickets/serializers/ticket/ticket.py:60 msgid "Organization" msgstr "组织" @@ -3873,7 +3993,7 @@ msgstr "组织 ({}) 的资产授权" msgid "Check asset permission expired" msgstr "校验资产授权规则已过期" -#: perms/tasks.py:39 +#: perms/tasks.py:40 msgid "Send asset permission expired notification" msgstr "发送资产权限过期通知" @@ -3909,27 +4029,27 @@ msgstr "{} 至少有一个系统角色" msgid "RBAC" msgstr "RBAC" -#: rbac/builtin.py:113 +#: rbac/builtin.py:114 msgid "SystemAdmin" msgstr "系统管理员" -#: rbac/builtin.py:116 +#: rbac/builtin.py:117 msgid "SystemAuditor" msgstr "系统审计员" -#: rbac/builtin.py:119 +#: rbac/builtin.py:120 msgid "SystemComponent" msgstr "系统组件" -#: rbac/builtin.py:125 +#: rbac/builtin.py:126 msgid "OrgAdmin" msgstr "组织管理员" -#: rbac/builtin.py:128 +#: rbac/builtin.py:129 msgid "OrgAuditor" msgstr "组织审计员" -#: rbac/builtin.py:131 +#: rbac/builtin.py:132 msgid "OrgUser" msgstr "组织用户" @@ -4074,24 +4194,24 @@ msgid "My assets" msgstr "我的资产" #: rbac/tree.py:56 terminal/models/applet/applet.py:51 -#: terminal/models/applet/applet.py:240 terminal/models/applet/host.py:28 +#: terminal/models/applet/applet.py:244 terminal/models/applet/host.py:28 #: terminal/serializers/applet.py:15 msgid "Applet" msgstr "远程应用" -#: rbac/tree.py:119 +#: rbac/tree.py:120 msgid "Ticket comment" msgstr "工单评论" -#: rbac/tree.py:120 tickets/models/ticket/general.py:307 +#: rbac/tree.py:121 tickets/models/ticket/general.py:307 msgid "Ticket" msgstr "工单管理" -#: rbac/tree.py:121 +#: rbac/tree.py:122 msgid "Common setting" msgstr "一般设置" -#: rbac/tree.py:122 +#: rbac/tree.py:123 msgid "View permission tree" msgstr "查看授权树" @@ -4104,23 +4224,23 @@ msgstr "测试成功" msgid "Test mail sent to {}, please check" msgstr "邮件已经发送{}, 请检查" -#: settings/api/ldap.py:173 +#: settings/api/ldap.py:176 msgid "Synchronization start, please wait." msgstr "同步开始,请稍等" -#: settings/api/ldap.py:177 +#: settings/api/ldap.py:180 msgid "Synchronization is running, please wait." msgstr "同步正在运行,请稍等" -#: settings/api/ldap.py:182 +#: settings/api/ldap.py:185 msgid "Synchronization error: {}" msgstr "同步错误: {}" -#: settings/api/ldap.py:220 +#: settings/api/ldap.py:223 msgid "Get ldap users is None" msgstr "获取 LDAP 用户为 None" -#: settings/api/ldap.py:230 +#: settings/api/ldap.py:233 msgid "Imported {} users successfully (Organization: {})" msgstr "成功导入 {} 个用户 ( 组织: {} )" @@ -5013,10 +5133,6 @@ msgstr "启用登录验证码" msgid "Enable captcha to prevent robot authentication" msgstr "开启验证码,防止机器人登录" -#: settings/serializers/security.py:151 -msgid "Security" -msgstr "安全" - #: settings/serializers/security.py:154 msgid "Enable terminal register" msgstr "终端注册" @@ -5169,104 +5285,104 @@ msgstr "提示:在Luna 页面中连接图形化资产时默认使用的分辨 msgid "Periodic import ldap user" msgstr "周期导入 LDAP 用户" -#: settings/tasks/ldap.py:47 +#: settings/tasks/ldap.py:46 msgid "Registration periodic import ldap user task" msgstr "注册周期导入 LDAP 用户 任务" -#: settings/utils/ldap.py:472 +#: settings/utils/ldap.py:476 msgid "ldap:// or ldaps:// protocol is used." msgstr "使用 ldap:// 或 ldaps:// 协议" -#: settings/utils/ldap.py:483 +#: settings/utils/ldap.py:487 msgid "Host or port is disconnected: {}" msgstr "主机或端口不可连接: {}" -#: settings/utils/ldap.py:485 +#: settings/utils/ldap.py:489 msgid "The port is not the port of the LDAP service: {}" msgstr "端口不是LDAP服务端口: {}" -#: settings/utils/ldap.py:487 +#: settings/utils/ldap.py:491 msgid "Please add certificate: {}" msgstr "请添加证书" -#: settings/utils/ldap.py:491 settings/utils/ldap.py:518 -#: settings/utils/ldap.py:548 settings/utils/ldap.py:576 +#: settings/utils/ldap.py:495 settings/utils/ldap.py:522 +#: settings/utils/ldap.py:552 settings/utils/ldap.py:580 msgid "Unknown error: {}" msgstr "未知错误: {}" -#: settings/utils/ldap.py:505 +#: settings/utils/ldap.py:509 msgid "Bind DN or Password incorrect" msgstr "绑定DN或密码错误" -#: settings/utils/ldap.py:512 +#: settings/utils/ldap.py:516 msgid "Please enter Bind DN: {}" msgstr "请输入绑定DN: {}" -#: settings/utils/ldap.py:514 +#: settings/utils/ldap.py:518 msgid "Please enter Password: {}" msgstr "请输入密码: {}" -#: settings/utils/ldap.py:516 +#: settings/utils/ldap.py:520 msgid "Please enter correct Bind DN and Password: {}" msgstr "请输入正确的绑定DN和密码: {}" -#: settings/utils/ldap.py:534 +#: settings/utils/ldap.py:538 msgid "Invalid User OU or User search filter: {}" msgstr "不合法的用户OU或用户过滤器: {}" -#: settings/utils/ldap.py:565 +#: settings/utils/ldap.py:569 msgid "LDAP User attr map not include: {}" msgstr "LDAP属性映射没有包含: {}" -#: settings/utils/ldap.py:572 +#: settings/utils/ldap.py:576 msgid "LDAP User attr map is not dict" msgstr "LDAP属性映射不合法" -#: settings/utils/ldap.py:591 +#: settings/utils/ldap.py:595 msgid "LDAP authentication is not enabled" msgstr "LDAP认证没有启用" -#: settings/utils/ldap.py:609 +#: settings/utils/ldap.py:613 msgid "Error (Invalid LDAP server): {}" msgstr "错误 (不合法的LDAP服务器地址): {}" -#: settings/utils/ldap.py:611 +#: settings/utils/ldap.py:615 msgid "Error (Invalid Bind DN): {}" msgstr "错误(不合法的绑定DN): {}" -#: settings/utils/ldap.py:613 +#: settings/utils/ldap.py:617 msgid "Error (Invalid LDAP User attr map): {}" msgstr "错误(不合法的LDAP属性映射): {}" -#: settings/utils/ldap.py:615 +#: settings/utils/ldap.py:619 msgid "Error (Invalid User OU or User search filter): {}" msgstr "错误(不合法的用户OU或用户过滤器): {}" -#: settings/utils/ldap.py:617 +#: settings/utils/ldap.py:621 msgid "Error (Not enabled LDAP authentication): {}" msgstr "错误(没有启用LDAP认证): {}" -#: settings/utils/ldap.py:619 +#: settings/utils/ldap.py:623 msgid "Error (Unknown): {}" msgstr "错误(未知): {}" -#: settings/utils/ldap.py:622 +#: settings/utils/ldap.py:626 msgid "Succeed: Match {} s user" msgstr "成功匹配 {} 个用户" -#: settings/utils/ldap.py:655 +#: settings/utils/ldap.py:659 msgid "Authentication failed (configuration incorrect): {}" msgstr "认证失败(配置错误): {}" -#: settings/utils/ldap.py:659 +#: settings/utils/ldap.py:663 msgid "Authentication failed (username or password incorrect): {}" msgstr "认证失败 (用户名或密码不正确): {}" -#: settings/utils/ldap.py:661 +#: settings/utils/ldap.py:665 msgid "Authentication failed (Unknown): {}" msgstr "认证失败: (未知): {}" -#: settings/utils/ldap.py:664 +#: settings/utils/ldap.py:668 msgid "Authentication success: {}" msgstr "认证成功: {}" @@ -5497,7 +5613,7 @@ msgstr "测试失败: {}" msgid "Test successful" msgstr "测试成功" -#: terminal/api/component/storage.py:124 terminal/notifications.py:179 +#: terminal/api/component/storage.py:124 terminal/notifications.py:240 #: terminal/tasks.py:144 msgid "Test failure: Account invalid" msgstr "测试失败: 账号无效" @@ -5526,60 +5642,69 @@ msgstr "未开启会话共享" msgid "Terminals" msgstr "终端管理" -#: terminal/backends/command/models.py:15 -msgid "Ordinary" -msgstr "普通" - -#: terminal/backends/command/models.py:16 -msgid "Dangerous" -msgstr "危险" - -#: terminal/backends/command/models.py:22 +#: terminal/backends/command/models.py:20 msgid "Input" msgstr "输入" -#: terminal/backends/command/models.py:23 terminal/serializers/command.py:48 +#: terminal/backends/command/models.py:21 terminal/serializers/command.py:74 msgid "Output" msgstr "输出" -#: terminal/backends/command/models.py:27 terminal/serializers/command.py:22 +#: terminal/backends/command/models.py:25 terminal/serializers/command.py:23 +#: terminal/templates/terminal/_msg_command_warning.html:10 msgid "Risk level" msgstr "风险等级" -#: terminal/connect_methods.py:55 +#: terminal/connect_methods.py:34 msgid "DB Client" msgstr "数据库客户端" -#: terminal/const.py:30 +#: terminal/const.py:12 +msgid "Review & Reject" +msgstr "审批 & 拒绝" + +#: terminal/const.py:13 +msgid "Review & Accept" +msgstr "审批 & 接受" + +#: terminal/const.py:14 +msgid "Review & Cancel" +msgstr "审批 & 取消" + +#: terminal/const.py:44 msgid "Critical" msgstr "严重" -#: terminal/const.py:31 +#: terminal/const.py:45 msgid "High" msgstr "较高" -#: terminal/const.py:32 terminal/const.py:66 +#: terminal/const.py:46 terminal/const.py:82 #: users/templates/users/reset_password.html:50 msgid "Normal" msgstr "正常" -#: terminal/const.py:33 +#: terminal/const.py:47 msgid "Offline" msgstr "离线" -#: terminal/const.py:62 +#: terminal/const.py:78 msgid "Mismatch" msgstr "未匹配" -#: terminal/const.py:67 +#: terminal/const.py:83 msgid "Tunnel" msgstr "隧道" -#: terminal/const.py:72 +#: terminal/const.py:85 +msgid "SFTP" +msgstr "SFTP" + +#: terminal/const.py:89 msgid "Read Only" msgstr "只读" -#: terminal/const.py:73 +#: terminal/const.py:90 msgid "Writable" msgstr "读写" @@ -5635,7 +5760,7 @@ msgstr "只支持自定义平台" msgid "Missing type in platform.yml" msgstr "在 platform.yml 中缺少类型" -#: terminal/models/applet/applet.py:242 terminal/models/applet/host.py:34 +#: terminal/models/applet/applet.py:246 terminal/models/applet/host.py:34 #: terminal/models/applet/host.py:134 msgid "Hosting" msgstr "宿主机" @@ -5693,18 +5818,18 @@ msgid "Redis port" msgstr "Redis 端口" #: terminal/models/component/endpoint.py:29 -#: terminal/models/component/endpoint.py:98 terminal/serializers/endpoint.py:73 -#: terminal/serializers/storage.py:38 terminal/serializers/storage.py:50 -#: terminal/serializers/storage.py:80 terminal/serializers/storage.py:90 -#: terminal/serializers/storage.py:98 +#: terminal/models/component/endpoint.py:100 +#: terminal/serializers/endpoint.py:73 terminal/serializers/storage.py:38 +#: terminal/serializers/storage.py:50 terminal/serializers/storage.py:80 +#: terminal/serializers/storage.py:90 terminal/serializers/storage.py:98 msgid "Endpoint" msgstr "端点" -#: terminal/models/component/endpoint.py:91 +#: terminal/models/component/endpoint.py:93 msgid "IP group" msgstr "IP 组" -#: terminal/models/component/endpoint.py:103 +#: terminal/models/component/endpoint.py:106 msgid "Endpoint rule" msgstr "端点规则" @@ -5754,7 +5879,7 @@ msgstr "录像存储" msgid "type" msgstr "类型" -#: terminal/models/component/terminal.py:89 terminal/serializers/command.py:51 +#: terminal/models/component/terminal.py:89 terminal/serializers/command.py:77 msgid "Remote Address" msgstr "远端地址" @@ -5782,39 +5907,39 @@ msgstr "可以上传会话录像" msgid "Can download session replay" msgstr "可以下载会话录像" -#: terminal/models/session/session.py:35 +#: terminal/models/session/session.py:34 msgid "Account id" msgstr "账号 ID" -#: terminal/models/session/session.py:37 terminal/models/session/sharing.py:104 +#: terminal/models/session/session.py:36 terminal/models/session/sharing.py:104 msgid "Login from" msgstr "登录来源" -#: terminal/models/session/session.py:42 +#: terminal/models/session/session.py:41 msgid "Replay" msgstr "回放" -#: terminal/models/session/session.py:46 +#: terminal/models/session/session.py:45 msgid "Date end" msgstr "结束日期" -#: terminal/models/session/session.py:243 +#: terminal/models/session/session.py:240 msgid "Session record" msgstr "会话记录" -#: terminal/models/session/session.py:245 +#: terminal/models/session/session.py:242 msgid "Can monitor session" msgstr "可以监控会话" -#: terminal/models/session/session.py:246 +#: terminal/models/session/session.py:243 msgid "Can share session" msgstr "可以分享会话" -#: terminal/models/session/session.py:247 +#: terminal/models/session/session.py:244 msgid "Can terminate session" msgstr "可以终断会话" -#: terminal/models/session/session.py:248 +#: terminal/models/session/session.py:245 msgid "Can validate session action perm" msgstr "可以验证会话动作权限" @@ -5871,31 +5996,35 @@ msgstr "验证码不正确" msgid "You have already joined this session" msgstr "您已经加入过此会话" -#: terminal/notifications.py:21 +#: terminal/notifications.py:25 msgid "Sessions" msgstr "会话管理" -#: terminal/notifications.py:67 -msgid "Danger command alert" -msgstr "危险命令告警" +#: terminal/notifications.py:72 +msgid "Command warning" +msgstr "命令告警" -#: terminal/notifications.py:97 terminal/notifications.py:145 +#: terminal/notifications.py:130 +msgid "Command reject" +msgstr "命令拒绝" + +#: terminal/notifications.py:157 terminal/notifications.py:206 msgid "Level" msgstr "级别" -#: terminal/notifications.py:115 +#: terminal/notifications.py:175 msgid "Batch danger command alert" msgstr "批量危险命令告警" -#: terminal/notifications.py:163 +#: terminal/notifications.py:224 msgid "Command and replay storage" msgstr "命令及录像存储" -#: terminal/notifications.py:164 +#: terminal/notifications.py:225 msgid "Connectivity alarm" msgstr "可连接性告警" -#: terminal/notifications.py:189 +#: terminal/notifications.py:250 #: terminal/templates/terminal/_msg_check_command_replay_storage_connectivity.html:4 msgid "Invalid storage" msgstr "无效的存储" @@ -5965,15 +6094,35 @@ msgstr "RDS 远程应用注销时间限制" msgid "Load status" msgstr "负载状态" -#: terminal/serializers/command.py:19 +#: terminal/serializers/command.py:20 msgid "Session ID" msgstr "会话ID" -#: terminal/serializers/command.py:47 +#: terminal/serializers/command.py:42 +msgid "Command Filter ACL" +msgstr "命令过滤器" + +#: terminal/serializers/command.py:45 +msgid "Command Group" +msgstr "命令组" + +#: terminal/serializers/command.py:56 +msgid "Invalid command filter ACL id" +msgstr "无效的 命令过滤器 ID" + +#: terminal/serializers/command.py:60 +msgid "Invalid command group id" +msgstr "无效的 命令组 ID" + +#: terminal/serializers/command.py:64 +msgid "Invalid session id" +msgstr "无效的 Session ID" + +#: terminal/serializers/command.py:73 msgid "Account " msgstr "账号" -#: terminal/serializers/command.py:49 +#: terminal/serializers/command.py:75 msgid "Timestamp" msgstr "时间戳" @@ -6017,35 +6166,35 @@ msgstr "如果不同端点下的资产 IP 有冲突,使用资产标签实现" msgid "Asset IP" msgstr "资产 IP" -#: terminal/serializers/session.py:25 terminal/serializers/session.py:47 +#: terminal/serializers/session.py:22 terminal/serializers/session.py:44 msgid "Can replay" msgstr "是否可重放" -#: terminal/serializers/session.py:26 terminal/serializers/session.py:48 +#: terminal/serializers/session.py:23 terminal/serializers/session.py:45 msgid "Can join" msgstr "是否可加入" -#: terminal/serializers/session.py:27 terminal/serializers/session.py:51 +#: terminal/serializers/session.py:24 terminal/serializers/session.py:48 msgid "Can terminate" msgstr "是否可中断" -#: terminal/serializers/session.py:43 +#: terminal/serializers/session.py:40 msgid "User ID" msgstr "用户 ID" -#: terminal/serializers/session.py:44 +#: terminal/serializers/session.py:41 msgid "Asset ID" msgstr "资产 ID" -#: terminal/serializers/session.py:45 +#: terminal/serializers/session.py:42 msgid "Login from display" msgstr "登录来源名称" -#: terminal/serializers/session.py:52 +#: terminal/serializers/session.py:49 msgid "Terminal display" msgstr "终端显示" -#: terminal/serializers/session.py:57 +#: terminal/serializers/session.py:54 msgid "Command amount" msgstr "命令数量" @@ -6147,7 +6296,7 @@ msgstr "检查命令及录像存储可连接性 " msgid "view" msgstr "查看" -#: terminal/utils/db_port_mapper.py:84 +#: terminal/utils/db_port_mapper.py:85 msgid "" "No available port is matched. The number of databases may have exceeded the " "number of ports open to the database agent service, Contact the " @@ -6156,13 +6305,13 @@ msgstr "" "未匹配到可用端口,数据库的数量可能已经超过数据库代理服务开放的端口数量,请联" "系管理员开放更多端口。" -#: terminal/utils/db_port_mapper.py:112 +#: terminal/utils/db_port_mapper.py:113 msgid "" "No ports can be used, check and modify the limit on the number of ports that " "Magnus listens on in the configuration file." msgstr "没有端口可以使用,检查并修改配置文件中 Magnus 监听的端口数量限制。" -#: terminal/utils/db_port_mapper.py:114 +#: terminal/utils/db_port_mapper.py:115 msgid "All available port count: {}, Already use port count: {}" msgstr "所有可用端口数量:{},已使用端口数量:{}" @@ -6356,11 +6505,11 @@ msgstr "审批步骤" msgid "Relation snapshot" msgstr "工单快照" -#: tickets/models/ticket/general.py:392 +#: tickets/models/ticket/general.py:398 msgid "Please try again" msgstr "请再次尝试" -#: tickets/models/ticket/general.py:461 +#: tickets/models/ticket/general.py:467 msgid "Super ticket" msgstr "超级工单" @@ -6437,7 +6586,7 @@ msgid "Apply actions" msgstr "申请动作" #: tickets/serializers/ticket/common.py:15 -#: tickets/serializers/ticket/common.py:77 +#: tickets/serializers/ticket/common.py:75 msgid "Created by ticket ({}-{})" msgstr "通过工单创建 ({}-{})" @@ -6445,7 +6594,7 @@ msgstr "通过工单创建 ({}-{})" msgid "The expiration date should be greater than the start date" msgstr "过期时间要大于开始时间" -#: tickets/serializers/ticket/common.py:84 +#: tickets/serializers/ticket/common.py:82 msgid "Permission named `{}` already exists" msgstr "授权名称 `{}` 已存在" @@ -6611,6 +6760,7 @@ msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" #: users/forms/profile.py:173 users/models/user.py:786 +#: xpack/plugins/cloud/serializers/account_attrs.py:206 msgid "Public key" msgstr "SSH公钥" @@ -6618,7 +6768,7 @@ msgstr "SSH公钥" msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:765 users/serializers/user.py:172 +#: users/models/user.py:765 users/serializers/user.py:171 msgid "Is service account" msgstr "服务账号" @@ -6630,7 +6780,7 @@ msgstr "头像" msgid "Wechat" msgstr "微信" -#: users/models/user.py:773 users/serializers/user.py:109 +#: users/models/user.py:773 users/serializers/user.py:108 msgid "Phone" msgstr "手机" @@ -6639,6 +6789,7 @@ msgid "OTP secret key" msgstr "OTP 密钥" #: users/models/user.py:783 +#: xpack/plugins/cloud/serializers/account_attrs.py:209 msgid "Private key" msgstr "ssh私钥" @@ -6647,7 +6798,7 @@ msgid "Secret key" msgstr "Secret key" #: users/models/user.py:794 users/serializers/profile.py:149 -#: users/serializers/user.py:169 +#: users/serializers/user.py:168 msgid "Is first login" msgstr "首次登录" @@ -6730,55 +6881,55 @@ msgstr "新密码不能是最近 {} 次的密码" msgid "The newly set password is inconsistent" msgstr "两次密码不一致" -#: users/serializers/user.py:43 +#: users/serializers/user.py:42 msgid "System roles" msgstr "系统角色" -#: users/serializers/user.py:47 +#: users/serializers/user.py:46 msgid "Org roles" msgstr "组织角色" -#: users/serializers/user.py:90 +#: users/serializers/user.py:89 msgid "Password strategy" msgstr "密码策略" -#: users/serializers/user.py:92 +#: users/serializers/user.py:91 msgid "MFA enabled" msgstr "MFA 已启用" -#: users/serializers/user.py:94 +#: users/serializers/user.py:93 msgid "MFA force enabled" msgstr "强制 MFA" -#: users/serializers/user.py:96 +#: users/serializers/user.py:95 msgid "Login blocked" msgstr "登录被锁定" -#: users/serializers/user.py:99 users/serializers/user.py:177 +#: users/serializers/user.py:98 users/serializers/user.py:176 msgid "Is OTP bound" msgstr "是否绑定了虚拟 MFA" -#: users/serializers/user.py:101 +#: users/serializers/user.py:100 msgid "Can public key authentication" msgstr "可以使用公钥认证" -#: users/serializers/user.py:174 +#: users/serializers/user.py:173 msgid "Avatar url" msgstr "头像路径" -#: users/serializers/user.py:178 +#: users/serializers/user.py:177 msgid "MFA level" msgstr "MFA 级别" -#: users/serializers/user.py:284 +#: users/serializers/user.py:283 msgid "Select users" msgstr "选择用户" -#: users/serializers/user.py:285 +#: users/serializers/user.py:284 msgid "For security, only list several users" msgstr "为了安全,仅列出几个用户" -#: users/serializers/user.py:318 +#: users/serializers/user.py:317 msgid "name not unique" msgstr "名称重复" @@ -7105,70 +7256,74 @@ msgid "Tencent Cloud (Lighthouse)" msgstr "腾讯云(轻量服务器应用)" #: xpack/plugins/cloud/const.py:19 -msgid "VMware" -msgstr "VMware" - -#: xpack/plugins/cloud/const.py:20 xpack/plugins/cloud/providers/nutanix.py:13 -msgid "Nutanix" -msgstr "Nutanix" - -#: xpack/plugins/cloud/const.py:21 -msgid "Huawei Private Cloud" -msgstr "华为私有云" - -#: xpack/plugins/cloud/const.py:22 -msgid "Qingyun Private Cloud" -msgstr "青云私有云" - -#: xpack/plugins/cloud/const.py:23 -msgid "CTYun Private Cloud" -msgstr "天翼私有云" - -#: xpack/plugins/cloud/const.py:24 -msgid "OpenStack" -msgstr "OpenStack" - -#: xpack/plugins/cloud/const.py:25 msgid "Google Cloud Platform" msgstr "谷歌云" +#: xpack/plugins/cloud/const.py:20 +msgid "UCloud" +msgstr "ucloud" + +#: xpack/plugins/cloud/const.py:22 +msgid "VMware" +msgstr "VMware" + +#: xpack/plugins/cloud/const.py:23 xpack/plugins/cloud/providers/nutanix.py:13 +msgid "Nutanix" +msgstr "Nutanix" + +#: xpack/plugins/cloud/const.py:24 +msgid "Huawei Private Cloud" +msgstr "华为私有云" + +#: xpack/plugins/cloud/const.py:25 +msgid "Qingyun Private Cloud" +msgstr "青云私有云" + #: xpack/plugins/cloud/const.py:26 +msgid "CTYun Private Cloud" +msgstr "天翼私有云" + +#: xpack/plugins/cloud/const.py:27 +msgid "OpenStack" +msgstr "OpenStack" + +#: xpack/plugins/cloud/const.py:28 msgid "Fusion Compute" msgstr "融合计算" -#: xpack/plugins/cloud/const.py:31 +#: xpack/plugins/cloud/const.py:33 msgid "Private IP" msgstr "私有IP" -#: xpack/plugins/cloud/const.py:32 +#: xpack/plugins/cloud/const.py:34 msgid "Public IP" msgstr "公网IP" -#: xpack/plugins/cloud/const.py:36 +#: xpack/plugins/cloud/const.py:38 msgid "Instance name" msgstr "实例名称" -#: xpack/plugins/cloud/const.py:37 +#: xpack/plugins/cloud/const.py:39 msgid "Instance name and Partial IP" msgstr "实例名称和部分IP" -#: xpack/plugins/cloud/const.py:42 +#: xpack/plugins/cloud/const.py:44 msgid "Succeed" msgstr "成功" -#: xpack/plugins/cloud/const.py:46 +#: xpack/plugins/cloud/const.py:48 msgid "Unsync" msgstr "未同步" -#: xpack/plugins/cloud/const.py:47 +#: xpack/plugins/cloud/const.py:49 msgid "New Sync" msgstr "新同步" -#: xpack/plugins/cloud/const.py:48 +#: xpack/plugins/cloud/const.py:50 msgid "Synced" msgstr "已同步" -#: xpack/plugins/cloud/const.py:49 +#: xpack/plugins/cloud/const.py:51 msgid "Released" msgstr "已释放" @@ -7434,11 +7589,11 @@ msgstr "华南-广州-友好用户环境" msgid "CN East-Suqian" msgstr "华东-宿迁" -#: xpack/plugins/cloud/serializers/account.py:63 +#: xpack/plugins/cloud/serializers/account.py:64 msgid "Validity display" msgstr "有效性显示" -#: xpack/plugins/cloud/serializers/account.py:64 +#: xpack/plugins/cloud/serializers/account.py:65 msgid "Provider display" msgstr "服务商显示" @@ -7458,6 +7613,7 @@ msgstr "订阅 ID" #: xpack/plugins/cloud/serializers/account_attrs.py:103 #: xpack/plugins/cloud/serializers/account_attrs.py:119 #: xpack/plugins/cloud/serializers/account_attrs.py:149 +#: xpack/plugins/cloud/serializers/account_attrs.py:202 msgid "API Endpoint" msgstr "API 端点" @@ -7522,6 +7678,10 @@ msgstr "测试端口" msgid "Test timeout" msgstr "测试超时时间" +#: xpack/plugins/cloud/serializers/account_attrs.py:212 +msgid "Project" +msgstr "project" + #: xpack/plugins/cloud/serializers/task.py:28 msgid "" "Only instances matching the IP range will be synced.
If the instance " @@ -7617,3 +7777,12 @@ msgstr "旗舰版" #: xpack/plugins/license/models.py:86 msgid "Community edition" msgstr "社区版" + +#~ msgid "Item" +#~ msgstr "项目" + +#~ msgid "Url" +#~ msgstr "链接" + +#~ msgid "Danger command alert" +#~ msgstr "危险命令告警" diff --git a/apps/manage.py b/apps/manage.py index d24c5fd38..17f2ce477 100755 --- a/apps/manage.py +++ b/apps/manage.py @@ -1,11 +1,10 @@ #!/usr/bin/env python import os import sys -import errno if __name__ == "__main__": try: - os.makedirs('../logs') + os.makedirs('../data/logs') except: pass diff --git a/apps/notifications/backends/email.py b/apps/notifications/backends/email.py index 390da151a..034557f8a 100644 --- a/apps/notifications/backends/email.py +++ b/apps/notifications/backends/email.py @@ -1,6 +1,4 @@ -from django.conf import settings -from django.core.mail import send_mail - +from common.tasks import send_mail_async from .base import BackendBase @@ -9,9 +7,8 @@ class Email(BackendBase): is_enable_field_in_settings = 'EMAIL_HOST_USER' def send_msg(self, users, message, subject): - from_email = settings.EMAIL_FROM or settings.EMAIL_HOST_USER accounts, __, __ = self.get_accounts(users) - send_mail(subject, message, from_email, accounts, html_message=message) + send_mail_async(subject, message, accounts, html_message=message) backend = Email diff --git a/apps/notifications/notifications.py b/apps/notifications/notifications.py index 6c631ec16..19a86ac94 100644 --- a/apps/notifications/notifications.py +++ b/apps/notifications/notifications.py @@ -194,7 +194,7 @@ class Message(metaclass=MessageType): return self.markdown_msg def get_feishu_msg(self) -> dict: - return self.text_msg + return self.markdown_msg def get_email_msg(self) -> dict: return self.html_msg_with_sign diff --git a/apps/notifications/signal_handlers.py b/apps/notifications/signal_handlers.py index 5b6b75a7e..797724755 100644 --- a/apps/notifications/signal_handlers.py +++ b/apps/notifications/signal_handlers.py @@ -72,8 +72,7 @@ def create_system_messages(app_config: AppConfig, **kwargs): sub, created = SystemMsgSubscription.objects.get_or_create(message_type=message_type) if created: obj.post_insert_to_db(sub) - logger.info( - f'Create SystemMsgSubscription: package={app_config.module.__package__} type={message_type}') + logger.info(f'Create MsgSubscription: package={app_config.module.__package__} type={message_type}') except ModuleNotFoundError: pass diff --git a/apps/ops/ansible/ansible.cfg b/apps/ops/ansible/ansible.cfg index c51b2d733..8e0d9918e 100644 --- a/apps/ops/ansible/ansible.cfg +++ b/apps/ops/ansible/ansible.cfg @@ -1,7 +1,8 @@ [defaults] -forks = 10 +forks = 10 host_key_checking = False -library = /opt/jumpserver/apps/ops/ansible/modules:./modules +library = /opt/jumpserver/apps/ops/ansible/modules:./modules +timeout = 65 [inventory] [privilege_escalation] [paramiko_connection] diff --git a/apps/ops/ansible/callback.py b/apps/ops/ansible/callback.py index 2045b6f29..d5fb3a35e 100644 --- a/apps/ops/ansible/callback.py +++ b/apps/ops/ansible/callback.py @@ -109,21 +109,21 @@ class DefaultCallback: pass def playbook_on_stats(self, event_data, **kwargs): - failed = [] error_func = lambda err, task_detail: err + f"{task_detail[0]}: {task_detail[1]['stderr']};" for tp in ['dark', 'failures']: for host, tasks in self.result[tp].items(): - failed.append(host) error = reduce(error_func, tasks.items(), '').strip(';') self.summary[tp][host] = error + failures = list(self.result['failures'].keys()) + dark_or_failures = list(self.result['dark'].keys()) + failures for host, tasks in self.result.get('ignored', {}).items(): ignore_errors = reduce(error_func, tasks.items(), '').strip(';') - if host in failed: - self.summary['failures'][host] += {ignore_errors} + if host in failures: + self.summary['failures'][host] += ignore_errors - self.summary['ok'] = list(set(self.result['ok'].keys()) - set(failed)) - self.summary['skipped'] = list(set(self.result['skipped'].keys()) - set(failed)) + self.summary['ok'] = list(set(self.result['ok'].keys()) - set(dark_or_failures)) + self.summary['skipped'] = list(set(self.result['skipped'].keys()) - set(dark_or_failures)) def playbook_on_include(self, event_data, **kwargs): pass diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py index 80922859b..9c0d03849 100644 --- a/apps/ops/ansible/inventory.py +++ b/apps/ops/ansible/inventory.py @@ -139,7 +139,7 @@ class JMSInventory: ansible_config['ansible_winrm_server_cert_validation'] = 'ignore' else: ansible_config['ansible_winrm_scheme'] = 'http' - ansible_config['ansible_winrm_transport'] = 'plaintext' + ansible_config['ansible_winrm_transport'] = 'ntlm' return ansible_config def asset_to_host(self, asset, account, automation, protocols, platform): @@ -268,6 +268,7 @@ class JMSInventory: data = {'all': {'hosts': {}}} for host in hosts: name = host.pop('name') + name = name.replace('[', '_').replace(']', '_') data['all']['hosts'][name] = host if self.exclude_localhost and data['all']['hosts'].__contains__('localhost'): data['all']['hosts'].update({'localhost': {'ansible_host': '255.255.255.255'}}) diff --git a/apps/ops/ansible/modules/rdp_ping.py b/apps/ops/ansible/modules/rdp_ping.py index f069b5bf7..6098f155d 100644 --- a/apps/ops/ansible/modules/rdp_ping.py +++ b/apps/ops/ansible/modules/rdp_ping.py @@ -39,10 +39,6 @@ import pyfreerdp from typing import NamedTuple from ansible.module_utils.basic import AnsibleModule -from ops.ansible.modules_utils.custom_common import ( - common_argument_spec -) - # ========================================= # Module execution. @@ -55,6 +51,18 @@ class Param(NamedTuple): password: str +def common_argument_spec(): + options = dict( + login_host=dict(type='str', required=False, default='localhost'), + login_port=dict(type='int', required=False, default=22), + login_user=dict(type='str', required=False, default='root'), + login_password=dict(type='str', required=False, no_log=True), + login_secret_type=dict(type='str', required=False, default='password'), + login_private_key_path=dict(type='str', required=False, no_log=True), + ) + return options + + def main(): options = common_argument_spec() module = AnsibleModule(argument_spec=options, supports_check_mode=True) diff --git a/apps/ops/ansible/runner.py b/apps/ops/ansible/runner.py index 03aec1787..4610ddfd2 100644 --- a/apps/ops/ansible/runner.py +++ b/apps/ops/ansible/runner.py @@ -5,6 +5,7 @@ import ansible_runner from django.conf import settings from .callback import DefaultCallback +from ..utils import get_ansible_log_verbosity class CommandInBlackListException(Exception): @@ -37,8 +38,7 @@ class AdHocRunner: def run(self, verbosity=0, **kwargs): self.check_module() - if verbosity is None and settings.DEBUG: - verbosity = 1 + verbosity = get_ansible_log_verbosity(verbosity) if not os.path.exists(self.project_dir): os.mkdir(self.project_dir, 0o755) @@ -70,8 +70,7 @@ class PlaybookRunner: self.cb = callback def run(self, verbosity=0, **kwargs): - if verbosity is None and settings.DEBUG: - verbosity = 1 + verbosity = get_ansible_log_verbosity(verbosity) ansible_runner.run( private_data_dir=self.project_dir, diff --git a/apps/ops/api/playbook.py b/apps/ops/api/playbook.py index d848e029c..9d74aa565 100644 --- a/apps/ops/api/playbook.py +++ b/apps/ops/api/playbook.py @@ -4,6 +4,7 @@ import zipfile from django.conf import settings from django.shortcuts import get_object_or_404 +from django.utils.translation import ugettext_lazy as _ from rest_framework import status from common.exceptions import JMSException @@ -12,7 +13,6 @@ from rbac.permissions import RBACPermission from ..exception import PlaybookNoValidEntry from ..models import Playbook from ..serializers.playbook import PlaybookSerializer -from django.utils.translation import ugettext_lazy as _ __all__ = ["PlaybookViewSet", "PlaybookFileBrowserAPIView"] @@ -33,7 +33,6 @@ class PlaybookViewSet(OrgBulkModelViewSet): search_fields = ('name', 'comment') def perform_destroy(self, instance): - instance = self.get_object() if instance.job_set.exists(): raise JMSException(code='playbook_has_job', detail={"msg": _("Currently playbook is being used in a job")}) instance_id = instance.id diff --git a/apps/ops/celery/beat/__init__.py b/apps/ops/celery/beat/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/ops/celery/beat/schedulers.py b/apps/ops/celery/beat/schedulers.py new file mode 100644 index 000000000..17bdcea3c --- /dev/null +++ b/apps/ops/celery/beat/schedulers.py @@ -0,0 +1,80 @@ +import logging +from celery.utils.log import get_logger +from django.db import close_old_connections +from django.core.exceptions import ObjectDoesNotExist +from django.db.utils import DatabaseError, InterfaceError + +from django_celery_beat.schedulers import DatabaseScheduler as DJDatabaseScheduler + +logger = get_logger(__name__) +debug, info, warning = logger.debug, logger.info, logger.warning + + +__all__ = ['DatabaseScheduler'] + + +class DatabaseScheduler(DJDatabaseScheduler): + + def sync(self): + if logger.isEnabledFor(logging.DEBUG): + debug('Writing entries...') + _tried = set() + _failed = set() + try: + close_old_connections() + + while self._dirty: + name = self._dirty.pop() + try: + # 源码 + # self.schedule[name].save() + # _tried.add(name) + + """ + ::Debug Description (2023.07.10):: + + 如果调用 self.schedule 可能会导致 self.save() 方法之前重新获取数据库中的数据, 而不是临时设置的 last_run_at 数据 + + 如果这里调用 self.schedule + 那么可能会导致调用 save 的 self.schedule[name] 的 last_run_at 是从数据库中获取回来的老数据 + 而不是任务执行后临时设置的 last_run_at (在 __next__() 方法中设置的) + 当 `max_interval` 间隔之后, 下一个任务检测周期还是会再次执行任务 + + ::Demo:: + 任务信息: + beat config: max_interval = 60s + + 任务名称: cap + 任务执行周期: 每 3 分钟执行一次 + 任务最后执行时间: 18:00 + + 任务第一次执行: 18:03 (执行时设置 last_run_at = 18:03, 此时在内存中) + + 任务执行完成后, + 检测到需要 sync, sync 中调用了 self.schedule, + self.schedule 中发现 schedule_changed() 为 True, 需要调用 all_as_schedule() + 此时,sync 中调用的 self.schedule[name] 的 last_run_at 是 18:00 + 这时候在 self.sync() 进行 self.save() + + + beat: Waking up 60s ... + + 任务第二次执行: 18:04 (因为获取回来的 last_run_at 是 18:00, entry.is_due() == True) + + ::解决方法:: + 所以这里为了避免从数据库中获取,直接使用 _schedule # + """ + self._schedule[name].save() + _tried.add(name) + except (KeyError, TypeError, ObjectDoesNotExist): + _failed.add(name) + except DatabaseError as exc: + logger.exception('Database error while sync: %r', exc) + except InterfaceError: + warning( + 'DatabaseScheduler: InterfaceError in sync(), ' + 'waiting to retry in next call...' + ) + finally: + # retry later, only for the failed ones + self._dirty |= _failed diff --git a/apps/ops/celery/signal_handler.py b/apps/ops/celery/signal_handler.py index 5ca29f7ba..bfd68508a 100644 --- a/apps/ops/celery/signal_handler.py +++ b/apps/ops/celery/signal_handler.py @@ -2,11 +2,11 @@ # import logging -from django.core.cache import cache from celery import subtask from celery.signals import ( worker_ready, worker_shutdown, after_setup_logger ) +from django.core.cache import cache from django_celery_beat.models import PeriodicTask from common.utils import get_logger diff --git a/apps/ops/models/job.py b/apps/ops/models/job.py index 7360838d7..06a858bd6 100644 --- a/apps/ops/models/job.py +++ b/apps/ops/models/job.py @@ -27,6 +27,8 @@ from orgs.mixins.models import JMSOrgBaseModel from perms.models import AssetPermission from perms.utils import UserPermAssetUtil from terminal.notifications import CommandExecutionAlert +from terminal.notifications import CommandWarningMessage +from terminal.const import RiskLevelChoices def get_parent_keys(key, include_self=True): @@ -394,10 +396,27 @@ class JobExecution(JMSOrgBaseModel): CommandExecutionAlert({ "assets": self.current_job.assets.all(), "input": self.material, - "risk_level": 5, + "risk_level": RiskLevelChoices.reject, "user": self.creator, }).publish_async() raise Exception("command is rejected by ACL") + elif acl.is_action(CommandFilterACL.ActionChoices.warning): + command = { + 'input': self.material, + 'user': self.creator.name, + 'asset': asset.name, + 'cmd_filter_acl': str(acl.id), + 'cmd_group': str(cg.id), + 'risk_level': RiskLevelChoices.warning, + 'org_id': self.org_id, + '_account': self.current_job.runas, + '_cmd_filter_acl': acl, + '_cmd_group': cg, + '_org_name': self.org_name, + } + for reviewer in acl.reviewers.all(): + CommandWarningMessage(reviewer, command).publish_async() + return True return False def check_command_acl(self): diff --git a/apps/ops/utils.py b/apps/ops/utils.py index c2fb7e643..539d6b66c 100644 --- a/apps/ops/utils.py +++ b/apps/ops/utils.py @@ -1,16 +1,11 @@ # ~*~ coding: utf-8 ~*~ import os import uuid +from django.conf import settings -from django.utils.translation import ugettext_lazy as _ - -from common.utils import get_logger, get_object_or_none, make_dirs -from orgs.utils import org_aware_func +from common.utils import get_logger, make_dirs from jumpserver.const import PROJECT_DIR -from .models import AdHoc, CeleryTask -from .const import DEFAULT_PASSWORD_RULES - logger = get_logger(__file__) @@ -26,3 +21,11 @@ def get_task_log_path(base_path, task_id, level=2): make_dirs(os.path.dirname(path), exist_ok=True) return path + +def get_ansible_log_verbosity(verbosity=0): + if settings.DEBUG_ANSIBLE: + return 10 + if verbosity is None and settings.DEBUG: + return 1 + return verbosity + diff --git a/apps/perms/api/asset_permission_relation.py b/apps/perms/api/asset_permission_relation.py index f2bd3cf30..5a97192e0 100644 --- a/apps/perms/api/asset_permission_relation.py +++ b/apps/perms/api/asset_permission_relation.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # +from functools import lru_cache + from rest_framework import generics from django.db.models import F from django.shortcuts import get_object_or_404 @@ -93,6 +95,7 @@ class AssetPermissionAllAssetListApi(generics.ListAPIView): filterset_fields = ("name", "address") search_fields = filterset_fields + @lru_cache(maxsize=2) def get_queryset(self): pk = self.kwargs.get("pk") assets = AssetPermissionPermAssetUtil(perm_ids=[pk]).get_all_assets() diff --git a/apps/perms/models/asset_permission.py b/apps/perms/models/asset_permission.py index c17a67acb..08484e88e 100644 --- a/apps/perms/models/asset_permission.py +++ b/apps/perms/models/asset_permission.py @@ -5,20 +5,20 @@ from django.db.models import Q from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from users.models import User -from assets.models import Asset +from accounts.const import AliasAccount from accounts.models import Account -from orgs.mixins.models import JMSOrgBaseModel -from orgs.mixins.models import OrgManager +from assets.models import Asset from common.utils import date_expired_default from common.utils.timezone import local_now +from orgs.mixins.models import JMSOrgBaseModel +from orgs.mixins.models import OrgManager from perms.const import ActionChoices -from accounts.const import AliasAccount +from users.models import User __all__ = ['AssetPermission', 'ActionChoices', 'AssetPermissionQuerySet'] # 使用场景 -logger = logging.getLogger(__name__) +logger = logging.getLogger('jumpserver.permissions') class AssetPermissionQuerySet(models.QuerySet): diff --git a/apps/perms/tasks.py b/apps/perms/tasks.py index 4420322d1..82f97bf8c 100644 --- a/apps/perms/tasks.py +++ b/apps/perms/tasks.py @@ -1,25 +1,25 @@ # ~*~ coding: utf-8 ~*~ from __future__ import absolute_import, unicode_literals -from datetime import timedelta + from collections import defaultdict +from datetime import timedelta -from django.db.transaction import atomic -from django.conf import settings from celery import shared_task +from django.conf import settings +from django.db.transaction import atomic +from django.utils.translation import gettext_lazy as _ -from ops.celery.decorator import register_as_period_task -from orgs.utils import tmp_to_root_org +from common.const.crontab import CRONTAB_AT_AM_TEN from common.utils import get_logger from common.utils.timezone import local_now, dt_parser -from common.const.crontab import CRONTAB_AT_AM_TEN - +from ops.celery.decorator import register_as_period_task +from orgs.utils import tmp_to_root_org from perms.models import AssetPermission -from perms.utils import UserPermTreeExpireUtil from perms.notifications import ( PermedAssetsWillExpireUserMsg, AssetPermsWillExpireForOrgAdminMsg, ) -from django.utils.translation import gettext_lazy as _ +from perms.utils import UserPermTreeExpireUtil logger = get_logger(__file__) @@ -32,7 +32,8 @@ def check_asset_permission_expired(): """ 这里的任务要足够短,不要影响周期任务 """ perms = AssetPermission.objects.get_expired_permissions() perm_ids = list(perms.distinct().values_list('id', flat=True)) - logger.info(f'Checking expired permissions: {perm_ids}') + show_perm_ids = perm_ids[:5] + logger.info(f'Checking expired permissions: {show_perm_ids} ...') UserPermTreeExpireUtil().expire_perm_tree_for_perms(perm_ids) diff --git a/apps/perms/utils/account.py b/apps/perms/utils/account.py index b66b247d8..d97343372 100644 --- a/apps/perms/utils/account.py +++ b/apps/perms/utils/account.py @@ -1,8 +1,8 @@ from collections import defaultdict -from orgs.utils import tmp_to_org -from accounts.models import Account from accounts.const import AliasAccount +from accounts.models import Account +from orgs.utils import tmp_to_org from .permission import AssetPermissionUtil __all__ = ['PermAccountUtil'] @@ -31,17 +31,20 @@ class PermAccountUtil(AssetPermissionUtil): @staticmethod def get_permed_accounts_from_perms(perms, user, asset): - # alias: is a collection of account usernames and special accounts [@ALL, @INPUT, @USER] + # alias: is a collection of account usernames and special accounts [@ALL, @INPUT, @USER, @ANON] alias_action_bit_mapper = defaultdict(int) - alias_expired_mapper = defaultdict(list) + alias_date_expired_mapper = defaultdict(list) for perm in perms: for alias in perm.accounts: alias_action_bit_mapper[alias] |= perm.actions - alias_expired_mapper[alias].append(perm.date_expired) + alias_date_expired_mapper[alias].append(perm.date_expired) asset_accounts = asset.accounts.all().active() - username_account_mapper = {account.username: account for account in asset_accounts} + # username_accounts_mapper = {account.username: account for account in asset_accounts} + username_accounts_mapper = defaultdict(list) + for account in asset_accounts: + username_accounts_mapper[account.username].append(account) cleaned_accounts_action_bit = defaultdict(int) cleaned_accounts_expired = defaultdict(list) @@ -52,25 +55,32 @@ class PermAccountUtil(AssetPermissionUtil): for account in asset_accounts: cleaned_accounts_action_bit[account] |= all_action_bit cleaned_accounts_expired[account].extend( - alias_expired_mapper[AliasAccount.ALL] + alias_date_expired_mapper[AliasAccount.ALL] ) for alias, action_bit in alias_action_bit_mapper.items(): + account = None + _accounts = [] if alias == AliasAccount.USER: - if user.username in username_account_mapper: - account = username_account_mapper[user.username] + if user.username in username_accounts_mapper: + _accounts = username_accounts_mapper[user.username] else: account = Account.get_user_account() elif alias == AliasAccount.INPUT: account = Account.get_manual_account() - elif alias in username_account_mapper: - account = username_account_mapper[alias] - else: - account = None + elif alias == AliasAccount.ANON: + account = Account.get_anonymous_account() + elif alias in username_accounts_mapper: + _accounts = username_accounts_mapper[alias] + elif alias.startswith('@'): + continue if account: + _accounts += [account] + + for account in _accounts: cleaned_accounts_action_bit[account] |= action_bit - cleaned_accounts_expired[account].extend(alias_expired_mapper[alias]) + cleaned_accounts_expired[account].extend(alias_date_expired_mapper[alias]) accounts = [] for account, action_bit in cleaned_accounts_action_bit.items(): diff --git a/apps/perms/utils/user_perm.py b/apps/perms/utils/user_perm.py index d054e67ce..7c78c9149 100644 --- a/apps/perms/utils/user_perm.py +++ b/apps/perms/utils/user_perm.py @@ -24,12 +24,9 @@ class AssetPermissionPermAssetUtil: def get_perm_nodes_assets(self, flat=False): """ 获取所有授权节点下的资产 """ - node_ids = AssetPermission.nodes.through.objects \ - .filter(assetpermission_id__in=self.perm_ids) \ - .values_list('node_id', flat=True) \ - .distinct() - node_ids = list(node_ids) - nodes = PermNode.objects.filter(id__in=node_ids).only('id', 'key') + from assets.models import Node + nodes = Node.objects.prefetch_related('granted_by_permissions').filter( + granted_by_permissions__in=self.perm_ids).only('id', 'key') assets = PermNode.get_nodes_all_assets(*nodes) if flat: return assets.values_list('id', flat=True) diff --git a/apps/rbac/builtin.py b/apps/rbac/builtin.py index cf80c64ab..508883446 100644 --- a/apps/rbac/builtin.py +++ b/apps/rbac/builtin.py @@ -26,11 +26,12 @@ user_perms = ( ) system_user_perms = ( - ('authentication', 'connectiontoken', 'add,change,view', 'connectiontoken'), - ('authentication', 'temptoken', 'add,change,view', 'temptoken'), - ('authentication', 'accesskey', '*', '*'), - ('tickets', 'ticket', 'view', 'ticket'), - ) + user_perms + _view_all_joined_org_perms + ('authentication', 'connectiontoken', 'add,view,reuse,expire', 'connectiontoken'), + ('authentication', 'temptoken', 'add,change,view', 'temptoken'), + ('authentication', 'accesskey', '*', '*'), + ('tickets', 'ticket', 'view', 'ticket'), +) +system_user_perms += (user_perms + _view_all_joined_org_perms) _auditor_perms = ( ('rbac', 'menupermission', 'view', 'audit'), @@ -151,7 +152,7 @@ class BuiltinRole: 'User': cls.system_user.get_role(), 'Auditor': cls.system_auditor.get_role() } - return cls.system_role_mapper[name] + return cls.system_role_mapper.get(name, cls.system_role_mapper['User']) @classmethod def get_org_role_by_old_name(cls, name): @@ -161,7 +162,7 @@ class BuiltinRole: 'User': cls.org_user.get_role(), 'Auditor': cls.org_auditor.get_role(), } - return cls.org_role_mapper[name] + return cls.org_role_mapper.get(name, cls.org_role_mapper['User']) @classmethod def sync_to_db(cls, show_msg=False): diff --git a/apps/rbac/const.py b/apps/rbac/const.py index 91ca727eb..cd7fd642a 100644 --- a/apps/rbac/const.py +++ b/apps/rbac/const.py @@ -22,7 +22,8 @@ exclude_permissions = ( ('common', 'setting', '*', '*'), ('authentication', 'privatetoken', '*', '*'), - ('authentication', 'connectiontoken', 'delete', 'connectiontoken'), + ('authentication', 'connectiontoken', 'delete,change', 'connectiontoken'), + ('authentication', 'connectiontoken', 'view', 'connectiontokensecret'), ('authentication', 'ssotoken', '*', '*'), ('authentication', 'superconnectiontoken', 'change,delete', 'superconnectiontoken'), ('authentication', 'temptoken', 'delete', 'temptoken'), diff --git a/apps/rbac/tree.py b/apps/rbac/tree.py index 1c9673162..a586208b6 100644 --- a/apps/rbac/tree.py +++ b/apps/rbac/tree.py @@ -65,6 +65,7 @@ special_pid_mapper = { 'acls.commandgroup': 'perms', 'acls.loginacl': 'perms', 'acls.loginassetacl': 'perms', + 'acls.connectmethodacl': 'perms', 'xpack.account': 'cloud_import', 'xpack.syncinstancedetail': 'cloud_import', 'xpack.syncinstancetask': 'cloud_import', @@ -390,7 +391,7 @@ class PermissionTreeUtil: 'chkDisabled': self.check_disabled, 'checked': checked, 'meta': { - 'type': tp, + 'type': tp, }, **data } diff --git a/apps/settings/api/ldap.py b/apps/settings/api/ldap.py index 256d21d31..8c98f821f 100644 --- a/apps/settings/api/ldap.py +++ b/apps/settings/api/ldap.py @@ -10,7 +10,7 @@ from rest_framework.generics import CreateAPIView from rest_framework.views import Response, APIView from common.api import AsyncApiMixin -from common.utils import get_logger, is_uuid +from common.utils import get_logger from orgs.models import Organization from orgs.utils import current_org from users.models import User @@ -166,6 +166,9 @@ class LDAPUserListApi(generics.ListAPIView): sync_util = LDAPSyncUtil() # 还没有同步任务 if sync_util.task_no_start: + ok, msg = LDAPTestUtil().test_config() + if not ok: + return Response(data={'msg': msg}, status=400) # 任务外部设置 task running 状态 sync_util.set_task_status(sync_util.TASK_STATUS_IS_RUNNING) t = threading.Thread(target=sync_ldap_user) diff --git a/apps/settings/api/settings.py b/apps/settings/api/settings.py index 6cc2743e2..4bcef2889 100644 --- a/apps/settings/api/settings.py +++ b/apps/settings/api/settings.py @@ -2,7 +2,12 @@ # from django.conf import settings +from django.http import HttpResponse +from django.views.static import serve from rest_framework import generics +from rest_framework import status +from rest_framework.permissions import AllowAny +from rest_framework.views import APIView from common.utils import get_logger from jumpserver.conf import Config @@ -10,6 +15,7 @@ from rbac.permissions import RBACPermission from .. import serializers from ..models import Setting from ..signals import category_setting_updated +from ..utils import get_interface_setting_or_default logger = get_logger(__file__) @@ -139,3 +145,25 @@ class SettingsApi(generics.RetrieveUpdateAPIView): if hasattr(serializer, 'post_save'): serializer.post_save() self.send_signal(serializer) + + +class SettingsLogoApi(APIView): + permission_classes = (AllowAny,) + + def get(self, request, *args, **kwargs): + size = request.GET.get('size', 'small') + interface_data = get_interface_setting_or_default() + if size == 'small': + logo_path = interface_data['logo_logout'] + else: + logo_path = interface_data['logo_index'] + + if logo_path.startswith('/media/'): + logo_path = logo_path.replace('/media/', '') + document_root = settings.MEDIA_ROOT + elif logo_path.startswith('/static/'): + logo_path = logo_path.replace('/static/', '/') + document_root = settings.STATIC_ROOT + else: + return HttpResponse(status=status.HTTP_404_NOT_FOUND) + return serve(request, logo_path, document_root=document_root) diff --git a/apps/settings/tasks/ldap.py b/apps/settings/tasks/ldap.py index fa2f4287c..b931efb48 100644 --- a/apps/settings/tasks/ldap.py +++ b/apps/settings/tasks/ldap.py @@ -23,7 +23,6 @@ def sync_ldap_user(): @shared_task(verbose_name=_('Periodic import ldap user')) -@transaction.atomic def import_ldap_user(): logger.info("Start import ldap user task") util_server = LDAPServerUtil() diff --git a/apps/settings/urls/api_urls.py b/apps/settings/urls/api_urls.py index ba04f2a4b..5cfc3bb36 100644 --- a/apps/settings/urls/api_urls.py +++ b/apps/settings/urls/api_urls.py @@ -20,6 +20,7 @@ urlpatterns = [ path('sms/backend/', api.SMSBackendAPI.as_view(), name='sms-backend'), path('setting/', api.SettingsApi.as_view(), name='settings-setting'), + path('logo/', api.SettingsLogoApi.as_view(), name='settings-logo'), path('public/', api.PublicSettingApi.as_view(), name='public-setting'), path('public/open/', api.OpenPublicSettingApi.as_view(), name='open-public-setting'), ] diff --git a/apps/settings/utils/ldap.py b/apps/settings/utils/ldap.py index 2d91306ad..e8ff0ab79 100644 --- a/apps/settings/utils/ldap.py +++ b/apps/settings/utils/ldap.py @@ -28,6 +28,7 @@ from authentication.backends.ldap import LDAPAuthorizationBackend, LDAPUser from common.const import LDAP_AD_ACCOUNT_DISABLE from common.db.utils import close_old_connections from common.utils import timeit, get_logger +from common.utils.http import is_true from orgs.utils import tmp_to_org from users.models import User, UserGroup from users.utils import construct_user_email @@ -185,9 +186,12 @@ class LDAPServerUtil(object): if not hasattr(entry, mapping): continue value = getattr(entry, mapping).value or '' - if attr == 'is_active' and mapping.lower() == 'useraccountcontrol' \ - and value: - value = int(value) & LDAP_AD_ACCOUNT_DISABLE != LDAP_AD_ACCOUNT_DISABLE + if attr == 'is_active': + if mapping.lower() == 'useraccountcontrol' and value: + value = int(value) & LDAP_AD_ACCOUNT_DISABLE != LDAP_AD_ACCOUNT_DISABLE + else: + value = is_true(value) + if attr == 'groups' and mapping.lower() == 'memberof': # AD: {'groups': 'memberOf'} if isinstance(value, str) and value: diff --git a/apps/terminal/api/session/command.py b/apps/terminal/api/session/command.py index b855181b8..4d20d345c 100644 --- a/apps/terminal/api/session/command.py +++ b/apps/terminal/api/session/command.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- # -from django.conf import settings +from django.utils import translation from django.utils import timezone from rest_framework import generics from rest_framework.fields import DateTimeField from rest_framework.response import Response +from acls.models import CommandFilterACL, CommandGroup from terminal.models import CommandStorage, Session, Command from terminal.filters import CommandFilter from orgs.utils import current_org @@ -18,7 +19,8 @@ from terminal.exceptions import StorageInvalid from terminal.backends import ( get_command_storage, get_multi_command_storage ) -from terminal.notifications import CommandAlertMessage +from terminal.notifications import CommandAlertMessage, CommandWarningMessage +from terminal.const import RiskLevelChoices logger = get_logger(__name__) __all__ = ['CommandViewSet', 'InsecureCommandAlertAPI'] @@ -199,7 +201,41 @@ class InsecureCommandAlertAPI(generics.CreateAPIView): serializer = InsecureCommandAlertSerializer(data=request.data, many=True) serializer.is_valid(raise_exception=True) commands = serializer.validated_data + session_ids, acl_ids, cmd_group_ids = set(), set(), set() for command in commands: - if command['risk_level'] >= settings.SECURITY_INSECURE_COMMAND_LEVEL: + session_ids.add(command.get('session')) + acl_ids.add(command.get('cmd_filter_acl')) + cmd_group_ids.add(command.get('cmd_group')) + + sessions = Session.objects.filter(id__in=session_ids).only( + 'id', 'org_id', 'asset', 'asset_id', 'user', 'user_id', 'account', 'account_id' + ) + session_mapper = {str(i.id): i for i in sessions} + acls = CommandFilterACL.objects.filter(id__in=acl_ids).only('id', 'name', 'reviewers') + acl_mapper = {str(i.id): i for i in acls} + cmd_groups = CommandGroup.objects.filter(id__in=cmd_group_ids).only('id', 'name') + cmd_group_mapper = {str(i.id): i for i in cmd_groups} + + for command in commands: + cmd_acl = acl_mapper.get(command['cmd_filter_acl']) + command['_cmd_filter_acl'] = cmd_acl + cmd_group = cmd_group_mapper.get(command['cmd_group']) + command['_cmd_group'] = cmd_group + session = session_mapper.get(command['session']) + risk_level = command.get('risk_level') + + if session: + command.update({ + '_account': session.account, + '_org_name': session.org.name, + }) + + if risk_level in [RiskLevelChoices.reject, RiskLevelChoices.review_reject]: CommandAlertMessage(command).publish_async() - return Response() + elif risk_level in [RiskLevelChoices.warning]: + for reviewer in cmd_acl.reviewers.all(): + CommandWarningMessage(reviewer, command).publish_async() + else: + logger.info(f'Risk level ignore: {RiskLevelChoices.get_label(risk_level)}({risk_level})') + + return Response({'msg': 'ok'}) diff --git a/apps/terminal/applets/chrome/ChangeLog b/apps/terminal/applets/chrome/ChangeLog new file mode 100644 index 000000000..7a03e6949 --- /dev/null +++ b/apps/terminal/applets/chrome/ChangeLog @@ -0,0 +1,3 @@ +# 2023-07-06 Version 0.5 +## 功能更新 + - 增加匿名用户的支持,如果账号是匿名用户,username 和 secret 则为空 diff --git a/apps/terminal/applets/chrome/README.md b/apps/terminal/applets/chrome/README.md index 068682bfb..cd03251ab 100644 --- a/apps/terminal/applets/chrome/README.md +++ b/apps/terminal/applets/chrome/README.md @@ -5,3 +5,6 @@ - Chrome 和 ChromeDriver 版本要匹配 - Driver [下载地址](https://chromedriver.chromium.org/downloads) +## ChangeLog + +一些重要的更新记录参考 [ChangeLog](./ChangeLog) diff --git a/apps/terminal/applets/chrome/app.py b/apps/terminal/applets/chrome/app.py index 9cc452590..47cce8844 100644 --- a/apps/terminal/applets/chrome/app.py +++ b/apps/terminal/applets/chrome/app.py @@ -135,6 +135,14 @@ class WebAPP(object): self.account = account self.platform = platform self._steps = list() + # 确保 account_username 和 account_secret 不为 None + self._account_username = account.username if account.username else '' + self._account_secret = account.secret if account.secret else '' + + # 如果是匿名账号,account_username 和 account_secret 为空 + if account.username == "@ANON": + self._account_username = '' + self._account_secret = '' extra_data = self.asset.spec_info autofill_type = extra_data.autofill @@ -153,23 +161,22 @@ class WebAPP(object): for item in steps: val = item.value if val: - val = val.replace("{USERNAME}", self.account.username) - val = val.replace("{SECRET}", self.account.secret) + val = val.replace("{USERNAME}", self._account_username) + val = val.replace("{SECRET}", self._account_secret) item.value = val self._steps.append(item) def _default_custom_steps(self, spec_info) -> list: - account = self.account - default_steps = [ + return [ Step({ "step": 1, - "value": account.username, + "value": self._account_username, "target": spec_info.username_selector, "command": "type" }), Step({ "step": 2, - "value": account.secret, + "value": self._account_secret, "target": spec_info.password_selector, "command": "type" }), @@ -180,7 +187,6 @@ class WebAPP(object): "command": "click" }) ] - return default_steps def execute(self, driver: webdriver.Chrome) -> bool: if not self.asset.address: diff --git a/apps/terminal/applets/chrome/extensions/disable_new_tab_window_menu/background.js b/apps/terminal/applets/chrome/extensions/disable_new_tab_window_menu/background.js index 57b0cca1a..7f2556ab8 100644 --- a/apps/terminal/applets/chrome/extensions/disable_new_tab_window_menu/background.js +++ b/apps/terminal/applets/chrome/extensions/disable_new_tab_window_menu/background.js @@ -1,23 +1,54 @@ // background.js +const tabs = [] +const debug = console.log + // 监听标签页的创建事件 chrome.tabs.onCreated.addListener(function (tab) { // 获取当前窗口的所有标签页 - chrome.tabs.query({currentWindow: true}, function (tabs) { - // 如果当前窗口的标签页数量大于1,则关闭新创建的标签页 - if (tabs.length > 1) { - chrome.tabs.remove(tab.id); - } - }); + debug('New tab add, tabs : ', tabs) + tabs.push(tab) }); -// 监听窗口的创建事件 -chrome.windows.onCreated.addListener(function (window) { -// 获取当前所有窗口 - chrome.windows.getAll(function (windows) { - // 如果当前窗口数量大于1,则关闭新创建的窗口 - if (windows.length > 1) { - chrome.windows.remove(window.id); - } - }); -}); +chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) { + debug('Tab changed xx: ', tabId, changeInfo, tab) + if (changeInfo.status !== 'loading') { + return + } + const tabFind = tabs.findIndex(t => t.id === tabId) + if (tabFind === -1) { + debug('Tab not found: ', tabId, tabs) + return + } + Object.assign(tabs[tabFind], tab) + + const blockUrls = ['chrome://newtab/'] + if (!tab.url || blockUrls.includes(tab.url) || tab.url.startsWith('chrome://')) { + debug('Blocked url, destroy: ', tab.url) + chrome.tabs.remove(tabId); + return + } + + // 第一个 tab 不做限制 + // 修改初始 tab 的状态,因为第一个 tab 没有地址栏,可以允许它自由跳转 + if (tabs.length === 1) { + debug('First tab, pass') + return + } + + const firstUrl = tabs[0].url + const curUrl = tab.url + if (!firstUrl.startsWith('http') || !curUrl.startsWith('http')) { + debug('First tab url empty, or current empty, pass ', firstUrl, curUrl) + return + } + + const firstTabHost = new URL(firstUrl).host + const curHost = new URL(curUrl).host + const firstDomain = firstTabHost.split('.').slice(-2).join('.') + const curDomain = curHost.split('.').slice(-2).join('.') + if (firstDomain !== curDomain) { + debug('Not same domain, destroy: ', firstTabHost, ' current: ', curHost) + chrome.tabs.remove(tabId); + } +}) diff --git a/apps/terminal/applets/chrome/extensions/disable_new_tab_window_menu/content_script.js b/apps/terminal/applets/chrome/extensions/disable_new_tab_window_menu/content_script.js index 0b6162430..3b2ba828c 100644 --- a/apps/terminal/applets/chrome/extensions/disable_new_tab_window_menu/content_script.js +++ b/apps/terminal/applets/chrome/extensions/disable_new_tab_window_menu/content_script.js @@ -1,5 +1,7 @@ // content_script.js +const debug = console.log + // 创建一个 Mutation Observer 实例 const observer = new MutationObserver(function (mutationsList) { // 遍历每个发生变化的 mutation @@ -10,7 +12,7 @@ const observer = new MutationObserver(function (mutationsList) { const links = document.getElementsByTagName('a'); // 遍历
标签元素并修改链接属性 - console.log("开始替换标签") + debug("开始替换标签") for (let i = 0; i < links.length; i++) { links[i].target = '_self'; // 将 target 属性设置为 _self,当前窗口打开 } @@ -27,43 +29,26 @@ const observer = new MutationObserver(function (mutationsList) { // 开始观察 document.body 的子节点变化 observer.observe(document.body, {childList: true, subtree: true}); -chrome.runtime.onMessage.addListener( - function (request, sender, sendResponse) { - console.log(request.url); - $("iframe").attr("src", request.url); - sendResponse({farewell: "goodbye"}); - } -) - document.addEventListener("contextmenu", function (event) { - console.log('On context') + debug('On context') event.preventDefault(); }); -var AllowedKeys = ['P', 'F', 'C', 'V'] +const AllowedKeys = ['P', 'F', 'C', 'V'] window.addEventListener("keydown", function (e) { if (e.key === "F12" || (e.ctrlKey && !AllowedKeys.includes(e.key.toUpperCase()))) { e.preventDefault(); e.stopPropagation(); - console.log('Press key: ', e.ctrlKey ? 'Ctrl' : '', e.shiftKey ? ' Shift' : '', e.key) + debug('Press key: ', e.ctrlKey ? 'Ctrl' : '', e.shiftKey ? ' Shift' : '', e.key) } }, true); -// 保存原始的 window.open 函数引用 -var originalOpen = window.open; - // 修改 window.open 函数 window.open = function (url, target, features) { // 将 target 强制设置为 "_self",使得新页面在当前标签页中打开 target = "_self"; - - // 修改当前页面的 URL - location.href = url; - + debug('Open url: ', url, target, features) // 调用原始的 window.open 函数 - return originalOpen.call(this, url, target, features); + window.href = url + // return originalOpen.call(this, url, target, features); }; - - -chrome.runtime.sendMessage({greeting: "hello"}, function (response) { -}); diff --git a/apps/terminal/applets/chrome/manifest.yml b/apps/terminal/applets/chrome/manifest.yml index 4c66515b6..066530eef 100644 --- a/apps/terminal/applets/chrome/manifest.yml +++ b/apps/terminal/applets/chrome/manifest.yml @@ -1,6 +1,6 @@ name: chrome display_name: "{{ 'Chrome Browser' | trans }}" -version: 0.4 +version: 0.6 comment: "{{ 'Chrome Browser Open URL Page Address' | trans }}" author: JumpServer Team exec_type: python diff --git a/apps/terminal/automations/deploy_applet_host/playbook.yml b/apps/terminal/automations/deploy_applet_host/playbook.yml index 3e63bd175..cd9bc655e 100644 --- a/apps/terminal/automations/deploy_applet_host/playbook.yml +++ b/apps/terminal/automations/deploy_applet_host/playbook.yml @@ -65,15 +65,15 @@ - '%USERPROFILE%\AppData\Local\Programs\Tinker\' scope: user - - name: Download python-3.10.8 + - name: Download python-3.10.11 ansible.windows.win_get_url: - url: "{{ APPLET_DOWNLOAD_HOST }}/download/applets/python-3.10.8-amd64.exe" - dest: "{{ ansible_env.TEMP }}\\python-3.10.8-amd64.exe" + url: "{{ APPLET_DOWNLOAD_HOST }}/download/applets/python-3.10.11-amd64.exe" + dest: "{{ ansible_env.TEMP }}\\python-3.10.11-amd64.exe" validate_certs: "{{ not IGNORE_VERIFY_CERTS }}" - - name: Install the python-3.10.8 + - name: Install the python-3.10.11 ansible.windows.win_package: - path: "{{ ansible_env.TEMP }}\\python-3.10.8-amd64.exe" + path: "{{ ansible_env.TEMP }}\\python-3.10.11-amd64.exe" product_id: '{371d0d73-d418-4ffe-b280-58c3e7987525}' arguments: - /quiet @@ -157,35 +157,43 @@ script: | pip install -r '{{ ansible_env.TEMP }}\pip_packages\pip_packages\requirements.txt' --no-index --find-links='{{ ansible_env.TEMP }}\pip_packages\pip_packages' - - name: Download chromedriver (Chromium) + - name: Download chromedriver (Chrome) ansible.windows.win_get_url: url: "{{ APPLET_DOWNLOAD_HOST }}/download/applets/chromedriver_win32.zip" dest: "{{ ansible_env.TEMP }}\\chromedriver_win32.zip" validate_certs: "{{ not IGNORE_VERIFY_CERTS }}" - - name: Unzip chromedriver (Chromium) + - name: Unzip chromedriver (Chrome) community.windows.win_unzip: src: "{{ ansible_env.TEMP }}\\chromedriver_win32.zip" - dest: C:\Program Files\JumpServer\drivers + dest: C:\Program Files\JumpServer\drivers\chromedriver_win32 - - name: Download chromium zip package (Chromium) + - name: Download Chrome zip package (Chrome) ansible.windows.win_get_url: url: "{{ APPLET_DOWNLOAD_HOST }}/download/applets/chrome-win.zip" dest: "{{ ansible_env.TEMP }}\\chrome-win.zip" validate_certs: "{{ not IGNORE_VERIFY_CERTS }}" - - name: Unzip Chromium (Chromium) + - name: Unzip Chrome (Chrome) community.windows.win_unzip: src: "{{ ansible_env.TEMP }}\\chrome-win.zip" - dest: C:\Program Files\Chrome + dest: C:\Program Files\JumpServer\applications - - name: Set chromium and driver on the global system path (Chromium) + - name: Check and Clean global system path (Chrome) ansible.windows.win_path: elements: - 'C:\Program Files\Chrome\chrome-win32' + - 'C:\Program Files\Chrome\chrome-win' + - 'C:\Program Files\chrome-win' + state: absent + + - name: Set Chrome and driver on the global system path (Chrome) + ansible.windows.win_path: + elements: + - 'C:\Program Files\JumpServer\applications\Chrome\Application' - 'C:\Program Files\JumpServer\drivers\chromedriver_win32' - - name: Set Chromium variables disable Google Api (Chromium) + - name: Set Chrome variables disable Google Api (Chrome) ansible.windows.win_environment: level: machine variables: @@ -221,4 +229,4 @@ - name: Sync all remote applets ansible.windows.win_powershell: script: | - tinkerd install all \ No newline at end of file + tinkerd install all diff --git a/apps/terminal/backends/command/models.py b/apps/terminal/backends/command/models.py index da535a17c..801b3cdf1 100644 --- a/apps/terminal/backends/command/models.py +++ b/apps/terminal/backends/command/models.py @@ -8,12 +8,10 @@ from django.utils.translation import ugettext_lazy as _ from common.utils.common import lazyproperty from orgs.mixins.models import OrgModelMixin +from terminal.const import RiskLevelChoices class AbstractSessionCommand(OrgModelMixin): - class RiskLevelChoices(models.IntegerChoices): - ordinary = 0, _('Ordinary') - dangerous = 5, _('Dangerous') id = models.UUIDField(default=uuid.uuid4, primary_key=True) user = models.CharField(max_length=64, db_index=True, verbose_name=_("User")) @@ -23,7 +21,7 @@ class AbstractSessionCommand(OrgModelMixin): output = models.CharField(max_length=1024, blank=True, verbose_name=_("Output")) session = models.CharField(max_length=36, db_index=True, verbose_name=_("Session")) risk_level = models.SmallIntegerField( - default=RiskLevelChoices.ordinary, choices=RiskLevelChoices.choices, db_index=True, + default=RiskLevelChoices.accept, choices=RiskLevelChoices.choices, db_index=True, verbose_name=_("Risk level") ) timestamp = models.IntegerField(db_index=True) @@ -44,11 +42,6 @@ class AbstractSessionCommand(OrgModelMixin): else: return '' - @classmethod - def get_risk_level_str(cls, risk_level): - risk_mapper = dict(cls.RiskLevelChoices.choices) - return risk_mapper.get(risk_level) - def to_dict(self): d = {} for field in self._meta.fields: diff --git a/apps/terminal/connect_methods.py b/apps/terminal/connect_methods.py index e38a8d123..f6870344d 100644 --- a/apps/terminal/connect_methods.py +++ b/apps/terminal/connect_methods.py @@ -17,31 +17,10 @@ class WebMethod(TextChoices): web_sftp = 'web_sftp', 'Web SFTP' @classmethod - def get_methods(cls): + def get_spec_methods(cls): methods = { Protocol.ssh: [cls.web_cli, cls.web_sftp], - Protocol.telnet: [cls.web_cli], - Protocol.rdp: [cls.web_gui], - Protocol.vnc: [cls.web_gui], - - Protocol.mysql: [cls.web_cli], - Protocol.mariadb: [cls.web_cli], - Protocol.oracle: [cls.web_cli], - Protocol.postgresql: [cls.web_cli], - Protocol.sqlserver: [cls.web_cli], - Protocol.redis: [cls.web_cli], - Protocol.mongodb: [cls.web_cli], - Protocol.clickhouse: [cls.web_cli], - - Protocol.k8s: [cls.web_cli], - Protocol.http: [] } - if not settings.XPACK_ENABLED: - return methods - - web_gui_dbs = [Protocol.mysql, Protocol.mariadb, Protocol.oracle, Protocol.postgresql] - for db in web_gui_dbs: - methods[db].append(cls.web_gui) return methods @@ -93,15 +72,11 @@ class NativeClient(TextChoices): def xpack_methods(cls): return [cls.mstsc] - @classmethod - def xpack_protocols(cls): - return [Protocol.rdp, Protocol.oracle, Protocol.clickhouse, Protocol.sqlserver] - @classmethod def get_methods(cls, os='windows'): clients_map = cls.get_native_clients() methods = defaultdict(list) - xpack_protocols = cls.xpack_protocols() + xpack_protocols = Protocol.xpack_protocols() for protocol, _clients in clients_map.items(): if not settings.XPACK_ENABLED and protocol in xpack_protocols: @@ -169,10 +144,10 @@ class ConnectMethodUtil: _all_methods = {} @classmethod - def protocols(cls): + def components(cls): protocols = { TerminalType.koko: { - 'web_methods': [WebMethod.web_cli, WebMethod.web_sftp], + 'web_methods': [WebMethod.web_cli], 'listen': [Protocol.http, Protocol.ssh], 'support': [ Protocol.ssh, Protocol.telnet, @@ -183,12 +158,13 @@ class ConnectMethodUtil: ], 'match': 'm2m' }, - TerminalType.omnidb: { + TerminalType.chen: { 'web_methods': [WebMethod.web_gui], 'listen': [Protocol.http], 'support': [ Protocol.mysql, Protocol.postgresql, - Protocol.oracle, Protocol.mariadb + Protocol.oracle, Protocol.sqlserver, + Protocol.mariadb ], 'match': 'm2m' }, @@ -214,6 +190,12 @@ class ConnectMethodUtil: 'support': [Protocol.rdp], 'match': 'map' }, + TerminalType.kael: { + 'web_methods': [WebMethod.web_gui], + 'listen': [Protocol.http], + 'support': [Protocol.chatgpt], + 'match': 'm2m' + } } return protocols @@ -279,16 +261,20 @@ class ConnectMethodUtil: return cls._all_methods['os'] methods = defaultdict(list) - web_methods = WebMethod.get_methods() + spec_web_methods = WebMethod.get_spec_methods() native_methods = NativeClient.get_methods(os) applet_methods = AppletMethod.get_methods() - for component, component_protocol in cls.protocols().items(): + for component, component_protocol in cls.components().items(): support = component_protocol['support'] component_web_methods = component_protocol.get('web_methods', []) for protocol in support: # Web 方式 + web_methods = spec_web_methods.get(protocol, None) + if web_methods is None: + web_methods = component_web_methods + methods[str(protocol)].extend([ { 'component': component.value, @@ -297,8 +283,7 @@ class ConnectMethodUtil: 'value': method.value, 'label': method.label, } - for method in web_methods.get(protocol, []) - if method in component_web_methods + for method in web_methods ]) # 客户端方式 @@ -306,6 +291,7 @@ class ConnectMethodUtil: listen = [protocol] else: listen = component_protocol['listen'] + for listen_protocol in listen: # Native method if component == TerminalType.koko and protocol.value != Protocol.ssh: @@ -321,7 +307,7 @@ class ConnectMethodUtil: for method in native_methods[listen_protocol] ]) - # 远程应用方式,这个只有 tinker 提供 + # 远程应用方式,这个只有 tinker 提供,并且协议可能是自定义的 for protocol, applet_methods in applet_methods.items(): for method in applet_methods: method['listen'] = 'rdp' diff --git a/apps/terminal/const.py b/apps/terminal/const.py index cfec290c7..f34233e3a 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -1,10 +1,24 @@ # -*- coding: utf-8 -*- # -from django.db.models import TextChoices +from django.db.models import TextChoices, IntegerChoices from django.utils.translation import ugettext_lazy as _ +class RiskLevelChoices(IntegerChoices): + accept = 0, _('Accept') + warning = 4, _('Warning') + reject = 5, _('Reject') + review_reject = 6, _('Review & Reject') + review_accept = 7, _('Review & Accept') + review_cancel = 8, _('Review & Cancel') + + @classmethod + def get_label(cls, level): + label = dict(cls.choices).get(level) + return label + + class ReplayStorageType(TextChoices): null = 'null', 'Null', server = 'server', 'Server' @@ -49,6 +63,8 @@ class TerminalType(TextChoices): razor = 'razor', 'Razor' tinker = 'tinker', 'Tinker' video_worker = 'video_worker', 'Video Worker' + chen = 'chen', 'Chen' + kael = 'kael', 'Kael' @classmethod def types(cls): @@ -66,6 +82,7 @@ class SessionType(TextChoices): normal = 'normal', _('Normal') tunnel = 'tunnel', _('Tunnel') command = 'command', _('Command') + sftp = 'sftp', _('SFTP') class ActionPermission(TextChoices): diff --git a/apps/terminal/migrations/0023_command_risk_level.py b/apps/terminal/migrations/0023_command_risk_level.py index 6ada1b826..0b5ed6bb6 100644 --- a/apps/terminal/migrations/0023_command_risk_level.py +++ b/apps/terminal/migrations/0023_command_risk_level.py @@ -13,6 +13,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='command', name='risk_level', - field=models.SmallIntegerField(choices=[(0, 'Ordinary'), (5, 'Dangerous')], db_index=True, default=0, verbose_name='Risk level'), + field=models.SmallIntegerField(choices=[(0, 'Accept'), (4, 'Warning'), (5, 'Reject'), (6, 'Review & Reject'), (7, 'Review & Accept'), (8, 'Review & Cancel')], db_index=True, default=0, verbose_name='Risk level'), ), ] diff --git a/apps/terminal/migrations/0050_auto_20220606_1745.py b/apps/terminal/migrations/0050_auto_20220606_1745.py index d0eb6ea5d..05e15f866 100644 --- a/apps/terminal/migrations/0050_auto_20220606_1745.py +++ b/apps/terminal/migrations/0050_auto_20220606_1745.py @@ -15,7 +15,7 @@ class Migration(migrations.Migration): field=models.CharField( choices=[('koko', 'KoKo'), ('guacamole', 'Guacamole'), ('omnidb', 'OmniDB'), ('xrdp', 'Xrdp'), ('lion', 'Lion'), ('core', 'Core'), ('celery', 'Celery'), ('magnus', 'Magnus'), - ('razor', 'Razor'), ('tinker', 'Tinker'), ('video_worker', 'Video Worker')], default='koko', - max_length=64, verbose_name='type'), + ('razor', 'Razor'), ('tinker', 'Tinker'), ('video_worker', 'Video Worker'), ('chen', 'Chen'), + ('kael', 'Kael')], default='koko', max_length=64, verbose_name='type'), ), ] diff --git a/apps/terminal/migrations/0063_auto_20230621_1133.py b/apps/terminal/migrations/0063_auto_20230621_1133.py new file mode 100644 index 000000000..85553e8f3 --- /dev/null +++ b/apps/terminal/migrations/0063_auto_20230621_1133.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.19 on 2023-06-21 10:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0062_applet_edition'), + ] + + operations = [ + migrations.AlterModelOptions( + name='endpointrule', + options={'ordering': ('priority', 'is_active', 'name'), 'verbose_name': 'Endpoint rule'}, + ), + migrations.AddField( + model_name='endpointrule', + name='is_active', + field=models.BooleanField(default=True, verbose_name='Is active'), + ), + ] diff --git a/apps/terminal/models/applet/applet.py b/apps/terminal/models/applet/applet.py index ef8eca8bc..edf7b332b 100644 --- a/apps/terminal/models/applet/applet.py +++ b/apps/terminal/models/applet/applet.py @@ -148,14 +148,18 @@ class Applet(JMSBaseModel): shutil.copytree(path, pkg_path) return instance, serializer - def select_host(self, user): - hosts = [ - host for host in self.hosts.filter(is_active=True) - if host.load != 'offline' - ] + def select_host(self, user, asset): + hosts = self.hosts.filter(is_active=True) + hosts = [host for host in hosts if host.load != 'offline'] if not hosts: return None + spec_label = asset.labels.filter(name__in=['AppletHost', '发布机']).first() + if spec_label: + host = [host for host in hosts if host.name == spec_label.value] + if host: + return host[0] + prefer_key = 'applet_host_prefer_{}'.format(user.id) prefer_host_id = cache.get(prefer_key, None) pref_host = [host for host in hosts if host.id == prefer_host_id] @@ -189,9 +193,9 @@ class Applet(JMSBaseModel): cache.set(prefer_host_account_key, account.id, timeout=None) return account - def select_host_account(self, user): + def select_host_account(self, user, asset): # 选择激活的发布机 - host = self.select_host(user) + host = self.select_host(user, asset) if not host: return None host_concurrent = str(host.deploy_options.get('RDS_fSingleSessionPerUser', 0)) == '0' diff --git a/apps/terminal/models/component/endpoint.py b/apps/terminal/models/component/endpoint.py index c5aab12d9..c68c06307 100644 --- a/apps/terminal/models/component/endpoint.py +++ b/apps/terminal/models/component/endpoint.py @@ -34,9 +34,11 @@ class Endpoint(JMSBaseModel): def get_port(self, target_instance, protocol): from terminal.utils import db_port_manager - from assets.const import DatabaseTypes + from assets.const import DatabaseTypes, Protocol + if isinstance(target_instance, Asset) and \ - target_instance.is_type(DatabaseTypes.ORACLE): + target_instance.is_type(DatabaseTypes.ORACLE) and \ + protocol == Protocol.oracle: port = db_port_manager.get_port_by_db(target_instance) else: port = getattr(self, f'{protocol}_port', 0) @@ -53,7 +55,7 @@ class Endpoint(JMSBaseModel): def is_valid_for(self, target_instance, protocol): if self.is_default(): return True - if self.host and self.get_port(target_instance, protocol) != 0: + if self.get_port(target_instance, protocol) != 0: return True return False @@ -98,17 +100,18 @@ class EndpointRule(JMSBaseModel): on_delete=models.SET_NULL, verbose_name=_("Endpoint"), ) comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) + is_active = models.BooleanField(default=True, verbose_name=_('Is active')) class Meta: verbose_name = _('Endpoint rule') - ordering = ('priority', 'name') + ordering = ('priority', 'is_active', 'name') def __str__(self): return f'{self.name}({self.priority})' @classmethod def match(cls, target_instance, target_ip, protocol): - for endpoint_rule in cls.objects.all().prefetch_related('endpoint'): + for endpoint_rule in cls.objects.prefetch_related('endpoint').filter(is_active=True): if not contains_ip(target_ip, endpoint_rule.ip_group): continue if not endpoint_rule.endpoint: diff --git a/apps/terminal/models/session/session.py b/apps/terminal/models/session/session.py index 5ddd6e647..ccf5936b8 100644 --- a/apps/terminal/models/session/session.py +++ b/apps/terminal/models/session/session.py @@ -10,12 +10,11 @@ from django.db import models from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from assets.const import Protocol from assets.models import Asset from common.utils import get_object_or_none, lazyproperty from orgs.mixins.models import OrgModelMixin from terminal.backends import get_multi_command_storage -from terminal.const import SessionType +from terminal.const import SessionType, TerminalType from users.models import User @@ -112,6 +111,7 @@ class Session(OrgModelMixin): return rel_path except: pass + @property def asset_obj(self): return Asset.objects.get(id=self.asset_id) @@ -132,10 +132,7 @@ class Session(OrgModelMixin): if self.type != SessionType.normal: # 会话监控仅支持 normal,不支持 tunnel 和 command return False - if self.protocol in [ - Protocol.ssh, Protocol.vnc, Protocol.rdp, - Protocol.telnet, Protocol.k8s - ]: + if self.terminal.type in [TerminalType.lion, TerminalType.koko]: return True else: return False diff --git a/apps/terminal/notifications.py b/apps/terminal/notifications.py index 919bf9c65..cfcac4396 100644 --- a/apps/terminal/notifications.py +++ b/apps/terminal/notifications.py @@ -9,13 +9,17 @@ from common.utils import lazyproperty from common.utils.timezone import local_now_display from notifications.backends import BACKEND from notifications.models import SystemMsgSubscription -from notifications.notifications import SystemMessage +from notifications.notifications import SystemMessage, UserMessage from terminal.models import Session, Command from users.models import User +from terminal.const import RiskLevelChoices logger = get_logger(__name__) -__all__ = ('CommandAlertMessage', 'CommandExecutionAlert', 'StorageConnectivityMessage') +__all__ = ( + 'CommandAlertMessage', 'CommandExecutionAlert', 'StorageConnectivityMessage', + 'CommandWarningMessage' +) CATEGORY = 'terminal' CATEGORY_LABEL = _('Sessions') @@ -26,13 +30,16 @@ class CommandAlertMixin: _get_message: Callable message_type_label: str + def __str__(self): + return str(self.message_type_label) + @lazyproperty def subject(self): _input = self.command['input'] if isinstance(_input, str): _input = _input.replace('\r\n', ' ').replace('\r', ' ').replace('\n', ' ') - subject = self.message_type_label + "%(cmd)s" % { + subject = self.message_type_label + ": %(cmd)s" % { 'cmd': _input } return subject @@ -61,17 +68,70 @@ class CommandAlertMixin: subscription.save() +class CommandWarningMessage(CommandAlertMixin, UserMessage): + message_type_label = _('Command warning') + + def __init__(self, user, command): + super().__init__(user) + self.command = command + + def get_html_msg(self) -> dict: + command = self.command + + command_input = command['input'] + user = command['user'] + asset = command['asset'] + account = command.get('_account', '') + cmd_acl = command.get('_cmd_filter_acl') + cmd_group = command.get('_cmd_group') + session_id = command.get('session', '') + risk_level = command['risk_level'] + org_id = command['org_id'] + org_name = command.get('_org_name') or org_id + + if session_id: + session_url = reverse( + 'api-terminal:session-detail', kwargs={'pk': session_id}, + external=True, api_to_ui=True + ) + '?oid={}'.format(org_id) + session_url = session_url.replace('/terminal/sessions/', '/audit/sessions/sessions/') + else: + session_url = '' + + # Command ACL + cmd_acl_name = cmd_group_name = '' + if cmd_acl: + cmd_acl_name = cmd_acl.name + if cmd_group: + cmd_group_name = cmd_group.name + + context = { + 'command': command_input, + 'user': user, + 'asset': asset, + 'account': account, + 'cmd_filter_acl': cmd_acl_name, + 'cmd_group': cmd_group_name, + 'session_url': session_url, + 'risk_level': RiskLevelChoices.get_label(risk_level), + 'org': org_name, + } + + message = render_to_string('terminal/_msg_command_warning.html', context) + return { + 'subject': self.subject, + 'message': message + } + + class CommandAlertMessage(CommandAlertMixin, SystemMessage): category = CATEGORY category_label = CATEGORY_LABEL - message_type_label = _('Danger command alert') + message_type_label = _('Command reject') def __init__(self, command): self.command = command - def __str__(self): - return str(self.message_type_label) - @classmethod def gen_test_msg(cls): command = Command.objects.first() @@ -90,7 +150,7 @@ class CommandAlertMessage(CommandAlertMixin, SystemMessage): session_detail_url = session_detail_url.replace( '/terminal/sessions/', '/audit/sessions/sessions/' ) - level = Command.get_risk_level_str(command['risk_level']) + level = RiskLevelChoices.get_label(command['risk_level']) items = { _("Asset"): command['asset'], _("User"): command['user'], @@ -139,7 +199,8 @@ class CommandExecutionAlert(CommandAlertMixin, SystemMessage): ) + '?oid={}'.format(asset.org_id) assets_with_url.append([asset, url]) - level = Command.get_risk_level_str(command['risk_level']) + level = RiskLevelChoices.get_label(command['risk_level']) + items = { _("User"): command['user'], _("Level"): level, diff --git a/apps/terminal/serializers/command.py b/apps/terminal/serializers/command.py index c58fd93e6..b8983a59c 100644 --- a/apps/terminal/serializers/command.py +++ b/apps/terminal/serializers/command.py @@ -2,11 +2,12 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.utils import pretty_string +from common.utils import pretty_string, is_uuid, get_logger from common.serializers.fields import LabeledChoiceField -from terminal.backends.command.models import AbstractSessionCommand from terminal.models import Command +from terminal.const import RiskLevelChoices +logger = get_logger(__name__) __all__ = ['SessionCommandSerializer', 'InsecureCommandAlertSerializer'] @@ -18,7 +19,7 @@ class SimpleSessionCommandSerializer(serializers.ModelSerializer): input = serializers.CharField(max_length=2048, label=_("Command")) session = serializers.CharField(max_length=36, label=_("Session ID")) risk_level = LabeledChoiceField( - choices=AbstractSessionCommand.RiskLevelChoices.choices, + choices=RiskLevelChoices.choices, required=False, label=_("Risk level"), ) org_id = serializers.CharField( @@ -37,7 +38,32 @@ class SimpleSessionCommandSerializer(serializers.ModelSerializer): class InsecureCommandAlertSerializer(SimpleSessionCommandSerializer): - pass + cmd_filter_acl = serializers.CharField( + max_length=36, required=False, label=_("Command Filter ACL") + ) + cmd_group = serializers.CharField( + max_length=36, required=True, label=_("Command Group") + ) + + class Meta(SimpleSessionCommandSerializer.Meta): + fields = SimpleSessionCommandSerializer.Meta.fields + [ + 'cmd_filter_acl', 'cmd_group', + ] + + def validate(self, attrs): + if not is_uuid(attrs['cmd_filter_acl']): + raise serializers.ValidationError( + _("Invalid command filter ACL id") + ) + if not is_uuid(attrs['cmd_group']): + raise serializers.ValidationError( + _("Invalid command group id") + ) + if not is_uuid(attrs['session']): + raise serializers.ValidationError( + _("Invalid session id") + ) + return super().validate(attrs) class SessionCommandSerializerMixin(serializers.Serializer): @@ -63,4 +89,3 @@ class SessionCommandSerializer(SessionCommandSerializerMixin, SimpleSessionComma fields = SimpleSessionCommandSerializer.Meta.fields + [ 'id', 'account', 'output', 'timestamp', 'timestamp_display', 'remote_addr' ] - diff --git a/apps/terminal/serializers/endpoint.py b/apps/terminal/serializers/endpoint.py index d7534adeb..82de09fce 100644 --- a/apps/terminal/serializers/endpoint.py +++ b/apps/terminal/serializers/endpoint.py @@ -79,7 +79,7 @@ class EndpointRuleSerializer(BulkModelSerializer): fields_small = fields_mini + ['ip_group', 'priority'] fields_fk = ['endpoint'] fields = fields_mini + fields_small + fields_fk + [ - 'comment', 'date_created', 'date_updated', 'created_by' + 'comment', 'date_created', 'date_updated', 'created_by', 'is_active' ] extra_kwargs = { 'priority': {'default': 50} diff --git a/apps/terminal/serializers/session.py b/apps/terminal/serializers/session.py index db7346d51..dc736a522 100644 --- a/apps/terminal/serializers/session.py +++ b/apps/terminal/serializers/session.py @@ -1,7 +1,6 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from assets.const import Protocol from common.serializers.fields import LabeledChoiceField from orgs.mixins.serializers import BulkOrgResourceModelSerializer from .terminal import TerminalSmallSerializer @@ -14,11 +13,9 @@ __all__ = [ ] - - class SessionSerializer(BulkOrgResourceModelSerializer): org_id = serializers.CharField(allow_blank=True) - protocol = serializers.ChoiceField(choices=Protocol.choices, label=_("Protocol")) + protocol = serializers.CharField(max_length=128, label=_("Protocol")) type = LabeledChoiceField( choices=SessionType.choices, label=_("Type"), default=SessionType.normal ) @@ -33,7 +30,7 @@ class SessionSerializer(BulkOrgResourceModelSerializer): "user", "asset", "user_id", "asset_id", 'account', 'account_id', "protocol", 'type', "login_from", "remote_addr", "is_success", "is_finished", "has_replay", "has_command", - "date_start", "date_end", "comment" + "date_start", "date_end", "comment", "terminal_display" ] fields_fk = ["terminal", ] fields_custom = ["can_replay", "can_join", "can_terminate"] diff --git a/apps/terminal/signal_handlers/db_port.py b/apps/terminal/signal_handlers/db_port.py index d6118d40e..5ea1c8aab 100644 --- a/apps/terminal/signal_handlers/db_port.py +++ b/apps/terminal/signal_handlers/db_port.py @@ -1,7 +1,7 @@ from django.db.models.signals import post_delete, post_save from django.dispatch import receiver -from assets.models import Asset +from assets.models import Asset, Database from common.decorators import on_transaction_commit from common.signals import django_ready from common.utils import get_logger @@ -12,16 +12,15 @@ logger = get_logger(__file__) @receiver(django_ready) def check_db_port_mapper(sender, **kwargs): - logger.info('Check oracle ports') + logger.info('Check oracle ports (MAGNUS_ORACLE_PORTS)') try: db_port_manager.check() except Exception as e: - pass + logger.error(e) -@receiver(post_save, sender=Asset) -@on_transaction_commit -def on_db_created(sender, instance: Asset, created, **kwargs): +@receiver(post_save, sender=Database) +def on_db_created(sender, instance: Database, created, **kwargs): if instance.type != 'oracle': return if not created: @@ -30,8 +29,7 @@ def on_db_created(sender, instance: Asset, created, **kwargs): db_port_manager.check() -@receiver(post_delete, sender=Asset) -@on_transaction_commit +@receiver(post_delete, sender=Database) def on_db_delete(sender, instance, **kwargs): if instance.type != 'oracle': return diff --git a/apps/terminal/templates/terminal/_msg_command_warning.html b/apps/terminal/templates/terminal/_msg_command_warning.html new file mode 100644 index 000000000..df7315341 --- /dev/null +++ b/apps/terminal/templates/terminal/_msg_command_warning.html @@ -0,0 +1,25 @@ +{% load i18n %} + +
+ {% trans 'Asset' %}: {{ asset }} +
+ {% trans 'User' %}: {{ user }} +
+ {% trans 'Account' %}: {{ account }} +
+ {% trans 'Risk level' %}: {{ risk_level }} +
+ {% trans 'Command acl' %}: {{ cmd_filter_acl }} +
+ {% trans 'Command group' %}: {{ cmd_group}} +
+ {% if session_url %} + {% trans 'Session' %}: +
{% trans 'View' %} +
+ {% endif %} + {% trans 'Organization' %}: {{ org }} +
+ {% trans 'Command' %}: {{ command }} +
+
diff --git a/apps/terminal/utils/db_port_mapper.py b/apps/terminal/utils/db_port_mapper.py index f5405f97e..9d335859a 100644 --- a/apps/terminal/utils/db_port_mapper.py +++ b/apps/terminal/utils/db_port_mapper.py @@ -79,6 +79,7 @@ class DBPortManager(object): for port, db_id in mapper.items(): if db_id == str(db.id): return port + if raise_exception: error = _( 'No available port is matched. ' diff --git a/apps/tickets/models/ticket/general.py b/apps/tickets/models/ticket/general.py index 45fdbac32..4d6c29483 100644 --- a/apps/tickets/models/ticket/general.py +++ b/apps/tickets/models/ticket/general.py @@ -4,7 +4,7 @@ import json from typing import Callable from django.db import models -from django.db.models import Q +from django.db.models import Prefetch, Q from django.db.models.fields import related from django.db.utils import IntegrityError from django.forms import model_to_dict @@ -326,7 +326,13 @@ class Ticket(StatusMixin, JMSBaseModel): @classmethod def get_user_related_tickets(cls, user): queries = Q(applicant=user) | Q(ticket_steps__ticket_assignees__assignee=user) - tickets = cls.objects.filter(queries).distinct() + # TODO: 与 StatusMixin.process_map 内连表查询有部分重叠 有优化空间 待验证排除是否不影响其它调用 + prefetch_ticket_assignee = Prefetch('ticket_steps__ticket_assignees', + queryset=TicketAssignee.objects.select_related('assignee'), ) + tickets = cls.objects.prefetch_related(prefetch_ticket_assignee)\ + .select_related('applicant')\ + .filter(queries)\ + .distinct() return tickets def get_current_ticket_flow_approve(self): diff --git a/apps/tickets/serializers/ticket/common.py b/apps/tickets/serializers/ticket/common.py index 1af361693..8ec463337 100644 --- a/apps/tickets/serializers/ticket/common.py +++ b/apps/tickets/serializers/ticket/common.py @@ -67,8 +67,6 @@ class BaseApplyAssetSerializer(serializers.Serializer): error = _('The expiration date should be greater than the start date') raise serializers.ValidationError({'apply_date_expired': error}) - attrs['apply_date_start'] = apply_date_start - attrs['apply_date_expired'] = apply_date_expired return attrs @atomic diff --git a/apps/users/utils.py b/apps/users/utils.py index 55ebd1aa1..29ce56cd1 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -1,20 +1,20 @@ # ~*~ coding: utf-8 ~*~ # -import os -import re -import pyotp import base64 import logging +import os +import re import time +import pyotp from django.conf import settings from django.core.cache import cache from common.tasks import send_mail_async -from common.utils import reverse, get_object_or_none, ip, pretty_string +from common.utils import reverse, get_object_or_none, ip from .models import User -logger = logging.getLogger('jumpserver') +logger = logging.getLogger('jumpserver.users') def send_user_created_mail(user): diff --git a/jms b/jms index c75011edd..0b2cf94d0 100755 --- a/jms +++ b/jms @@ -152,10 +152,14 @@ def start_services(): start_args = [] if args.daemon: start_args.append('--daemon') - if args.worker: - start_args.extend(['--worker', str(args.worker)]) if args.force: start_args.append('--force') + if args.worker: + start_args.extend(['--worker', str(args.worker)]) + else: + worker = os.environ.get('CORE_WORKER') + if isinstance(worker, str) and worker.isdigit(): + start_args.extend(['--worker', worker]) try: management.call_command(action, *services, *start_args) diff --git a/logs/.gitkeep b/logs/.gitkeep deleted file mode 100644 index 1a4baf536..000000000 --- a/logs/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/requirements/issues.txt b/requirements/issues.txt index 5db4004a7..768d95e1e 100644 --- a/requirements/issues.txt +++ b/requirements/issues.txt @@ -1,6 +1,6 @@ # Install cryptography error: build/temp.macosx-10.13-intel-2.7/_openssl.c:483:10: fatal error: 'openssl/opensslv.h' file not found -$ pip install cryptography --global-option=build_ext --global-option="-L/usr/local/opt/openssl/lib" --global-option="-I/usr/local/opt/openssl/include" +$ pip install cryptography --global-option=build_ext --global-option="-L/opt/homebrew/Cellar/openssl@3/3.1.1_1/lib" --global-option="-I/opt/homebrew/Cellar/openssl@3/3.1.1_1/include" # Pillow zlib failed @@ -9,3 +9,7 @@ Reinstall xcode reslove $ xcode-select --install + +# libxmlsec 报错 +wget 'https://raw.githubusercontent.com/Homebrew/homebrew-core/7f35e6ede954326a10949891af2dba47bbe1fc17/Formula/libxmlsec1.rb' +brew install ./libxmlsec1.rb diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 33bc01dbe..65004e605 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -16,7 +16,7 @@ ecdsa==0.13.3 enum-compat==0.0.2 ephem==3.7.6.0 future==0.16.0 -idna==2.6 +idna==2.8 itypes==1.2.0 Jinja2==3.1.2 jmespath==1.0.1 @@ -37,7 +37,7 @@ PyNaCl==1.5.0 python-dateutil==2.8.2 PyYAML==6.0 requests==2.31.0 -jms-storage==0.0.46 +jms-storage==0.0.47 simplejson==3.17.6 six==1.16.0 sshtunnel==0.4.0 @@ -68,7 +68,7 @@ geoip2==4.5.0 ipip-ipdb==1.6.1 pywinrm==0.4.3 # Django environment -Django==3.2.19 +Django==3.2.20 django-bootstrap3==14.2.0 django-filter==2.4.0 django-formtools==2.2 diff --git a/requirements/requirements_xpack.txt b/requirements/requirements_xpack.txt index c7d48e644..8f1930fad 100644 --- a/requirements/requirements_xpack.txt +++ b/requirements/requirements_xpack.txt @@ -23,3 +23,4 @@ psycopg2-binary==2.9.1 pymssql==2.2.5 IPy==1.1 psycopg2==2.9.4 +ucloud-sdk-python3==0.11.47 diff --git a/utils/clean_host_to_device.py b/utils/clean_host_to_device.py new file mode 100644 index 000000000..0bc767b6a --- /dev/null +++ b/utils/clean_host_to_device.py @@ -0,0 +1,37 @@ +import os +import sys + +import django + +if os.path.exists('../apps'): + sys.path.insert(0, '../apps') +elif os.path.exists('./apps'): + sys.path.insert(0, './apps') + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jumpserver.settings") +django.setup() + +from assets.models import Asset as asset_model, Host as host_model, Device as device_model +from orgs.models import Organization + + +def clean_host(): + root = Organization.root() + root.change_to() + + devices = host_model.objects.filter(platform__category='device') + assets = asset_model.objects.filter(id__in=devices.values_list('asset_ptr_id', flat=True)) + assets_map = {asset.id: asset for asset in assets} + + for host in devices: + asset = assets_map.get(host.asset_ptr_id) + if not asset: + continue + device = device_model(asset_ptr_id=asset.id) + device.__dict__.update(asset.__dict__) + device.save() + host.delete(keep_parents=True) + + +if __name__ == "__main__": + clean_host() diff --git a/utils/start_celery_beat.py b/utils/start_celery_beat.py index 23aa1cb83..19d578e70 100644 --- a/utils/start_celery_beat.py +++ b/utils/start_celery_beat.py @@ -54,7 +54,7 @@ else: connection_params['port'] = settings.REDIS_PORT redis_client = Redis(**connection_params) -scheduler = "django_celery_beat.schedulers:DatabaseScheduler" +scheduler = "ops.celery.beat.schedulers:DatabaseScheduler" processes = [] cmd = [ 'celery',