mirror of https://github.com/jumpserver/jumpserver
commit
25987545db
|
@ -0,0 +1,24 @@
|
|||
name: Publish Release to Discord
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
send_discord_notification:
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.event.release.tag_name, 'v4.')
|
||||
steps:
|
||||
- name: Send release notification to Discord
|
||||
env:
|
||||
WEBHOOK_URL: ${{ secrets.DISCORD_CHANGELOG_WEBHOOK }}
|
||||
run: |
|
||||
# 获取标签名称和 release body
|
||||
TAG_NAME="${{ github.event.release.tag_name }}"
|
||||
RELEASE_BODY="${{ github.event.release.body }}"
|
||||
|
||||
# 使用 jq 构建 JSON 数据,以确保安全传递
|
||||
JSON_PAYLOAD=$(jq -n --arg tag "# JumpServer $TAG_NAME Released! 🚀" --arg body "$RELEASE_BODY" '{content: "\($tag)\n\($body)"}')
|
||||
|
||||
# 使用 curl 发送 JSON 数据
|
||||
curl -X POST -H "Content-Type: application/json" -d "$JSON_PAYLOAD" "$WEBHOOK_URL"
|
|
@ -0,0 +1,28 @@
|
|||
name: LLM Code Review
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
jobs:
|
||||
llm-code-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: fit2cloud/LLM-CodeReview-Action@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.FIT2CLOUDRD_LLM_CODE_REVIEW_TOKEN }}
|
||||
OPENAI_API_KEY: ${{ secrets.ALIYUN_LLM_API_KEY }}
|
||||
LANGUAGE: English
|
||||
OPENAI_API_ENDPOINT: https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
MODEL: qwen2-1.5b-instruct
|
||||
PROMPT: "Please check the following code differences for any irregularities, potential issues, or optimization suggestions, and provide your answers in English."
|
||||
top_p: 1
|
||||
temperature: 1
|
||||
# max_tokens: 10000
|
||||
MAX_PATCH_LENGTH: 10000
|
||||
IGNORE_PATTERNS: "/node_modules,*.md,/dist,/.github"
|
||||
FILE_PATTERNS: "*.java,*.go,*.py,*.vue,*.ts,*.js,*.css,*.scss,*.html"
|
13
Dockerfile
13
Dockerfile
|
@ -1,4 +1,4 @@
|
|||
FROM jumpserver/core-base:20240924_031841 AS stage-build
|
||||
FROM jumpserver/core-base:20241105_025649 AS stage-build
|
||||
|
||||
ARG VERSION
|
||||
|
||||
|
@ -24,10 +24,10 @@ ENV LANG=en_US.UTF-8 \
|
|||
PATH=/opt/py3/bin:$PATH
|
||||
|
||||
ARG DEPENDENCIES=" \
|
||||
libldap2-dev \
|
||||
libx11-dev"
|
||||
|
||||
ARG TOOLS=" \
|
||||
cron \
|
||||
ca-certificates \
|
||||
default-libmysqlclient-dev \
|
||||
openssh-client \
|
||||
|
@ -35,19 +35,20 @@ ARG TOOLS=" \
|
|||
bubblewrap"
|
||||
|
||||
ARG APT_MIRROR=http://deb.debian.org
|
||||
|
||||
RUN set -ex \
|
||||
&& rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& apt-get update > /dev/null \
|
||||
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${TOOLS} \
|
||||
&& apt-get clean \
|
||||
&& mkdir -p /root/.ssh/ \
|
||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \
|
||||
&& echo "no" | dpkg-reconfigure dash \
|
||||
&& sed -i "s@# export @export @g" ~/.bashrc \
|
||||
&& sed -i "s@# alias @alias @g" ~/.bashrc
|
||||
&& apt-get clean all \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& echo "0 3 * * * root find /tmp -type f -mtime +1 -size +1M -exec rm -f {} \; && date > /tmp/clean.log" > /etc/cron.d/cleanup_tmp \
|
||||
&& chmod 0644 /etc/cron.d/cleanup_tmp
|
||||
|
||||
COPY --from=stage-build /opt /opt
|
||||
COPY --from=stage-build /usr/local/bin /usr/local/bin
|
||||
|
|
|
@ -15,8 +15,8 @@ ARG DEPENDENCIES=" \
|
|||
libldap2-dev \
|
||||
libsasl2-dev"
|
||||
|
||||
|
||||
ARG APT_MIRROR=http://deb.debian.org
|
||||
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core \
|
||||
set -ex \
|
||||
|
@ -27,9 +27,8 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
|||
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
||||
&& echo "no" | dpkg-reconfigure dash
|
||||
|
||||
|
||||
# Install bin tools
|
||||
ARG CHECK_VERSION=v1.0.3
|
||||
ARG CHECK_VERSION=v1.0.4
|
||||
RUN set -ex \
|
||||
&& wget https://github.com/jumpserver-dev/healthcheck/releases/download/${CHECK_VERSION}/check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
|
||||
&& tar -xf check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
|
||||
|
@ -38,23 +37,24 @@ RUN set -ex \
|
|||
&& chmod 755 /usr/local/bin/check \
|
||||
&& rm -f check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz
|
||||
|
||||
|
||||
# Install Python dependencies
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
ARG PIP_MIRROR=https://pypi.org/simple
|
||||
ENV POETRY_PYPI_MIRROR_URL=${PIP_MIRROR}
|
||||
ENV ANSIBLE_COLLECTIONS_PATHS=/opt/py3/lib/python3.11/site-packages/ansible_collections
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache,sharing=locked,id=core \
|
||||
RUN --mount=type=cache,target=/root/.cache \
|
||||
--mount=type=bind,source=poetry.lock,target=poetry.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
--mount=type=bind,source=utils/clean_site_packages.sh,target=clean_site_packages.sh \
|
||||
--mount=type=bind,source=requirements/collections.yml,target=collections.yml \
|
||||
set -ex \
|
||||
&& python3 -m venv /opt/py3 \
|
||||
&& pip install poetry -i ${PIP_MIRROR} \
|
||||
&& poetry config virtualenvs.create false \
|
||||
&& pip install poetry poetry-plugin-pypi-mirror -i ${PIP_MIRROR} \
|
||||
&& . /opt/py3/bin/activate \
|
||||
&& poetry install --only main \
|
||||
&& poetry config virtualenvs.create false \
|
||||
&& poetry install --no-cache --only main \
|
||||
&& ansible-galaxy collection install -r collections.yml --force --ignore-certs \
|
||||
&& bash clean_site_packages.sh
|
||||
&& bash clean_site_packages.sh \
|
||||
&& poetry cache clear pypi --all
|
||||
|
|
|
@ -15,21 +15,20 @@ ARG TOOLS=" \
|
|||
vim \
|
||||
wget"
|
||||
|
||||
ARG APT_MIRROR=http://deb.debian.org
|
||||
RUN set -ex \
|
||||
&& rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
|
||||
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install --no-install-recommends ${TOOLS} \
|
||||
&& echo "no" | dpkg-reconfigure dash
|
||||
&& apt-get clean all \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
ARG PIP_MIRROR=https://pypi.org/simple
|
||||
ENV POETRY_PYPI_MIRROR_URL=${PIP_MIRROR}
|
||||
COPY poetry.lock pyproject.toml ./
|
||||
RUN set -ex \
|
||||
&& . /opt/py3/bin/activate \
|
||||
&& pip install poetry -i ${PIP_MIRROR} \
|
||||
&& poetry install --only xpack
|
||||
&& pip install poetry poetry-plugin-pypi-mirror -i ${PIP_MIRROR} \
|
||||
&& poetry install --only xpack \
|
||||
&& poetry cache clear pypi --all
|
||||
|
||||
|
|
|
@ -30,6 +30,6 @@
|
|||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_secret_type: "{{ account.secret_type }}"
|
||||
gateway_args: "{{ jms_gateway | default(None) }}"
|
||||
gateway_args: "{{ jms_gateway | default({}) }}"
|
||||
when: account.secret_type == "password"
|
||||
delegate_to: localhost
|
||||
|
|
|
@ -30,6 +30,6 @@
|
|||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_secret_type: "{{ account.secret_type }}"
|
||||
gateway_args: "{{ jms_gateway | default(None) }}"
|
||||
gateway_args: "{{ jms_gateway | default({}) }}"
|
||||
when: account.secret_type == "password"
|
||||
delegate_to: localhost
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
from importlib import import_module
|
||||
|
||||
from django.utils.functional import LazyObject
|
||||
from django.utils.functional import LazyObject, empty
|
||||
|
||||
from common.utils import get_logger
|
||||
from ..const import VaultTypeChoices
|
||||
|
||||
__all__ = ['vault_client', 'get_vault_client']
|
||||
|
||||
__all__ = ['vault_client', 'get_vault_client', 'refresh_vault_client']
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
def get_vault_client(raise_exception=False, **kwargs):
|
||||
enabled = kwargs.get('VAULT_ENABLED')
|
||||
tp = 'hcp' if enabled else 'local'
|
||||
tp = kwargs.get('VAULT_BACKEND') if kwargs.get('VAULT_ENABLED') else VaultTypeChoices.local
|
||||
|
||||
# TODO: Temporary processing, subsequent deletion
|
||||
tp = VaultTypeChoices.local if tp == VaultTypeChoices.azure else tp
|
||||
|
||||
try:
|
||||
module_path = f'apps.accounts.backends.{tp}.main'
|
||||
client = import_module(module_path).Vault(**kwargs)
|
||||
|
@ -39,3 +41,7 @@ class VaultClient(LazyObject):
|
|||
|
||||
""" 为了安全, 页面修改配置, 重启服务后才会重新初始化 vault_client """
|
||||
vault_client = VaultClient()
|
||||
|
||||
|
||||
def refresh_vault_client():
|
||||
vault_client._wrapped = empty
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
from .main import *
|
|
@ -0,0 +1,70 @@
|
|||
import sys
|
||||
from abc import ABC
|
||||
|
||||
from common.db.utils import Encryptor
|
||||
from common.utils import lazyproperty
|
||||
|
||||
current_module = sys.modules[__name__]
|
||||
|
||||
__all__ = ['build_entry']
|
||||
|
||||
|
||||
class BaseEntry(ABC):
|
||||
|
||||
def __init__(self, instance):
|
||||
self.instance = instance
|
||||
|
||||
@lazyproperty
|
||||
def full_path(self):
|
||||
return self.path_spec
|
||||
|
||||
@property
|
||||
def path_spec(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def to_internal_data(self):
|
||||
secret = getattr(self.instance, '_secret', None)
|
||||
if secret is not None:
|
||||
secret = Encryptor(secret).encrypt()
|
||||
return secret
|
||||
|
||||
@staticmethod
|
||||
def to_external_data(secret):
|
||||
if secret is not None:
|
||||
secret = Encryptor(secret).decrypt()
|
||||
return secret
|
||||
|
||||
|
||||
class AccountEntry(BaseEntry):
|
||||
|
||||
@property
|
||||
def path_spec(self):
|
||||
# 长度 0-127
|
||||
account_id = str(self.instance.id)[:18]
|
||||
path = f'assets-{self.instance.asset_id}-accounts-{account_id}'
|
||||
return path
|
||||
|
||||
|
||||
class AccountTemplateEntry(BaseEntry):
|
||||
|
||||
@property
|
||||
def path_spec(self):
|
||||
path = f'account-templates-{self.instance.id}'
|
||||
return path
|
||||
|
||||
|
||||
class HistoricalAccountEntry(BaseEntry):
|
||||
|
||||
@property
|
||||
def path_spec(self):
|
||||
path = f'accounts-{self.instance.instance.id}-histories-{self.instance.history_id}'
|
||||
return path
|
||||
|
||||
|
||||
def build_entry(instance) -> BaseEntry:
|
||||
class_name = instance.__class__.__name__
|
||||
entry_class_name = f'{class_name}Entry'
|
||||
entry_class = getattr(current_module, entry_class_name, None)
|
||||
if not entry_class:
|
||||
raise Exception(f'Entry class {entry_class_name} is not found')
|
||||
return entry_class(instance)
|
|
@ -0,0 +1,57 @@
|
|||
from common.db.utils import get_logger
|
||||
from .entries import build_entry
|
||||
from .service import AZUREVaultClient
|
||||
from ..base import BaseVault
|
||||
|
||||
from ...const import VaultTypeChoices
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
__all__ = ['Vault']
|
||||
|
||||
|
||||
class Vault(BaseVault):
|
||||
type = VaultTypeChoices.azure
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.client = AZUREVaultClient(
|
||||
vault_url=kwargs.get('VAULT_AZURE_HOST'),
|
||||
tenant_id=kwargs.get('VAULT_AZURE_TENANT_ID'),
|
||||
client_id=kwargs.get('VAULT_AZURE_CLIENT_ID'),
|
||||
client_secret=kwargs.get('VAULT_AZURE_CLIENT_SECRET')
|
||||
)
|
||||
|
||||
def is_active(self):
|
||||
return self.client.is_active()
|
||||
|
||||
def _get(self, instance):
|
||||
entry = build_entry(instance)
|
||||
secret = self.client.get(name=entry.full_path)
|
||||
secret = entry.to_external_data(secret)
|
||||
return secret
|
||||
|
||||
def _create(self, instance):
|
||||
entry = build_entry(instance)
|
||||
secret = entry.to_internal_data()
|
||||
self.client.create(name=entry.full_path, secret=secret)
|
||||
|
||||
def _update(self, instance):
|
||||
entry = build_entry(instance)
|
||||
secret = entry.to_internal_data()
|
||||
self.client.update(name=entry.full_path, secret=secret)
|
||||
|
||||
def _delete(self, instance):
|
||||
entry = build_entry(instance)
|
||||
self.client.delete(name=entry.full_path)
|
||||
|
||||
def _clean_db_secret(self, instance):
|
||||
instance.is_sync_metadata = False
|
||||
instance.mark_secret_save_to_vault()
|
||||
|
||||
def _save_metadata(self, instance, metadata):
|
||||
try:
|
||||
entry = build_entry(instance)
|
||||
self.client.update_metadata(name=entry.full_path, metadata=metadata)
|
||||
except Exception as e:
|
||||
logger.error(f'save metadata error: {e}')
|
|
@ -0,0 +1,59 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from azure.core.exceptions import ResourceNotFoundError, ClientAuthenticationError
|
||||
from azure.identity import ClientSecretCredential
|
||||
from azure.keyvault.secrets import SecretClient
|
||||
|
||||
from common.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
__all__ = ['AZUREVaultClient']
|
||||
|
||||
|
||||
class AZUREVaultClient(object):
|
||||
|
||||
def __init__(self, vault_url, tenant_id, client_id, client_secret):
|
||||
authentication_endpoint = 'https://login.microsoftonline.com/' \
|
||||
if ('azure.net' in vault_url) else 'https://login.chinacloudapi.cn/'
|
||||
|
||||
credentials = ClientSecretCredential(
|
||||
client_id=client_id, client_secret=client_secret, tenant_id=tenant_id, authority=authentication_endpoint
|
||||
)
|
||||
self.client = SecretClient(vault_url=vault_url, credential=credentials)
|
||||
|
||||
def is_active(self):
|
||||
try:
|
||||
self.client.set_secret('jumpserver', '666')
|
||||
except (ResourceNotFoundError, ClientAuthenticationError) as e:
|
||||
logger.error(str(e))
|
||||
return False, f'Vault is not reachable: {e}'
|
||||
else:
|
||||
return True, ''
|
||||
|
||||
def get(self, name, version=None):
|
||||
try:
|
||||
secret = self.client.get_secret(name, version)
|
||||
return secret.value
|
||||
except (ResourceNotFoundError, ClientAuthenticationError) as e:
|
||||
logger.error(f'get: {name} {str(e)}')
|
||||
return ''
|
||||
|
||||
def create(self, name, secret):
|
||||
if not secret:
|
||||
secret = ''
|
||||
self.client.set_secret(name, secret)
|
||||
|
||||
def update(self, name, secret):
|
||||
if not secret:
|
||||
secret = ''
|
||||
self.client.set_secret(name, secret)
|
||||
|
||||
def delete(self, name):
|
||||
self.client.begin_delete_secret(name)
|
||||
|
||||
def update_metadata(self, name, metadata: dict):
|
||||
try:
|
||||
self.client.update_secret_properties(name, tags=metadata)
|
||||
except (ResourceNotFoundError, ClientAuthenticationError) as e:
|
||||
logger.error(f'update_metadata: {name} {str(e)}')
|
|
@ -10,6 +10,11 @@ class BaseVault(ABC):
|
|||
def __init__(self, *args, **kwargs):
|
||||
self.enabled = kwargs.get('VAULT_ENABLED')
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def type(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def get(self, instance):
|
||||
""" 返回 secret 值 """
|
||||
return self._get(instance)
|
||||
|
@ -20,9 +25,6 @@ class BaseVault(ABC):
|
|||
self._clean_db_secret(instance)
|
||||
self.save_metadata(instance)
|
||||
|
||||
if instance.is_sync_metadata:
|
||||
self.save_metadata(instance)
|
||||
|
||||
def update(self, instance):
|
||||
if not instance.secret_has_save_to_vault:
|
||||
self._update(instance)
|
||||
|
|
|
@ -3,12 +3,16 @@ from .entries import build_entry
|
|||
from .service import VaultKVClient
|
||||
from ..base import BaseVault
|
||||
|
||||
__all__ = ['Vault']
|
||||
from ...const import VaultTypeChoices
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
__all__ = ['Vault']
|
||||
|
||||
|
||||
class Vault(BaseVault):
|
||||
type = VaultTypeChoices.hcp
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.client = VaultKVClient(
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from common.utils import get_logger
|
||||
from ..base import BaseVault
|
||||
from ...const import VaultTypeChoices
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
@ -7,6 +8,7 @@ __all__ = ['Vault']
|
|||
|
||||
|
||||
class Vault(BaseVault):
|
||||
type = VaultTypeChoices.local
|
||||
|
||||
def is_active(self):
|
||||
return True, ''
|
||||
|
|
|
@ -49,9 +49,9 @@ class SecretStrategy(models.TextChoices):
|
|||
|
||||
|
||||
class SSHKeyStrategy(models.TextChoices):
|
||||
add = 'add', _('Append SSH KEY')
|
||||
set = 'set', _('Empty and append SSH KEY')
|
||||
set_jms = 'set_jms', _('Replace (Replace only keys pushed by JumpServer) ')
|
||||
set = 'set', _('Empty and append SSH KEY')
|
||||
add = 'add', _('Append SSH KEY')
|
||||
|
||||
|
||||
class TriggerChoice(models.TextChoices, TreeChoices):
|
||||
|
|
|
@ -7,3 +7,4 @@ __all__ = ['VaultTypeChoices']
|
|||
class VaultTypeChoices(models.TextChoices):
|
||||
local = 'local', _('Database')
|
||||
hcp = 'hcp', _('HCP Vault')
|
||||
azure = 'azure', _('Azure Key Vault')
|
||||
|
|
|
@ -50,7 +50,7 @@ class Migration(migrations.Migration):
|
|||
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
|
||||
('secret_strategy', models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy')),
|
||||
('password_rules', models.JSONField(default=dict, verbose_name='Password rules')),
|
||||
('ssh_key_change_strategy', models.CharField(choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), ('set_jms', 'Replace (Replace only keys pushed by JumpServer) ')], default='add', max_length=16, verbose_name='SSH key change strategy')),
|
||||
('ssh_key_change_strategy', models.CharField(choices=[('set_jms', 'Replace (Replace only keys pushed by JumpServer) '), ('set', 'Empty and append SSH KEY'), ('add', 'Append SSH KEY')], default='set_jms', max_length=16, verbose_name='SSH key change strategy')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Change secret automation',
|
||||
|
@ -76,7 +76,7 @@ class Migration(migrations.Migration):
|
|||
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
|
||||
('secret_strategy', models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy')),
|
||||
('password_rules', models.JSONField(default=dict, verbose_name='Password rules')),
|
||||
('ssh_key_change_strategy', models.CharField(choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), ('set_jms', 'Replace (Replace only keys pushed by JumpServer) ')], default='add', max_length=16, verbose_name='SSH key change strategy')),
|
||||
('ssh_key_change_strategy', models.CharField(choices=[('set_jms', 'Replace (Replace only keys pushed by JumpServer) '), ('set', 'Empty and append SSH KEY'), ('add', 'Append SSH KEY')], default='set_jms', max_length=16, verbose_name='SSH key change strategy')),
|
||||
('triggers', models.JSONField(default=list, max_length=16, verbose_name='Triggers')),
|
||||
('username', models.CharField(max_length=128, verbose_name='Username')),
|
||||
('action', models.CharField(max_length=16, verbose_name='Action')),
|
||||
|
|
|
@ -51,7 +51,7 @@ class AutomationExecution(AssetAutomationExecution):
|
|||
class ChangeSecretMixin(SecretWithRandomMixin):
|
||||
ssh_key_change_strategy = models.CharField(
|
||||
choices=SSHKeyStrategy.choices, max_length=16,
|
||||
default=SSHKeyStrategy.add, verbose_name=_('SSH key change strategy')
|
||||
default=SSHKeyStrategy.set_jms, verbose_name=_('SSH key change strategy')
|
||||
)
|
||||
get_all_assets: callable # get all assets
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ from django.conf import settings
|
|||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.const import AutomationTypes, SecretType
|
||||
from accounts.models import Account
|
||||
from .base import AccountBaseAutomation
|
||||
from .change_secret import ChangeSecretMixin
|
||||
|
@ -23,7 +23,8 @@ class PushAccountAutomation(ChangeSecretMixin, AccountBaseAutomation):
|
|||
create_usernames = set(usernames) - set(account_usernames)
|
||||
create_account_objs = [
|
||||
Account(
|
||||
name=f'{username}-{secret_type}', username=username,
|
||||
name=f"{username}-{secret_type}" if secret_type != SecretType.PASSWORD else username,
|
||||
username=username,
|
||||
secret_type=secret_type, asset=asset,
|
||||
)
|
||||
for username in create_usernames
|
||||
|
|
|
@ -63,6 +63,26 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
|
|||
)},
|
||||
}}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.set_ssh_key_change_strategy_choices()
|
||||
|
||||
def set_ssh_key_change_strategy_choices(self):
|
||||
ssh_key_change_strategy = self.fields.get("ssh_key_change_strategy")
|
||||
if not ssh_key_change_strategy:
|
||||
return
|
||||
ssh_key_change_strategy._choices.pop(SSHKeyStrategy.add, None)
|
||||
|
||||
def to_representation(self, instance):
|
||||
data = super().to_representation(instance)
|
||||
ssh_strategy_value = data.get('ssh_key_change_strategy', {}).get('value')
|
||||
if ssh_strategy_value == SSHKeyStrategy.add:
|
||||
data['ssh_key_change_strategy'] = {
|
||||
'label': SSHKeyStrategy.set_jms.label,
|
||||
'value': SSHKeyStrategy.set_jms.value
|
||||
}
|
||||
return data
|
||||
|
||||
@property
|
||||
def model_type(self):
|
||||
return AutomationTypes.change_secret
|
||||
|
|
|
@ -3,14 +3,17 @@ from collections import defaultdict
|
|||
from django.db.models.signals import post_delete
|
||||
from django.db.models.signals import pre_save, post_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils.functional import LazyObject
|
||||
from django.utils.translation import gettext_noop
|
||||
|
||||
from accounts.backends import vault_client
|
||||
from accounts.backends import vault_client, refresh_vault_client
|
||||
from accounts.const import Source
|
||||
from audits.const import ActivityChoices
|
||||
from audits.signal_handlers import create_activities
|
||||
from common.decorators import merge_delay_run
|
||||
from common.signals import django_ready
|
||||
from common.utils import get_logger, i18n_fmt
|
||||
from common.utils.connection import RedisPubSub
|
||||
from .models import Account, AccountTemplate
|
||||
from .tasks.push_account import push_accounts_to_assets_task
|
||||
|
||||
|
@ -91,3 +94,18 @@ class VaultSignalHandler(object):
|
|||
for model in (Account, AccountTemplate, Account.history.model):
|
||||
post_save.connect(VaultSignalHandler.save_to_vault, sender=model)
|
||||
post_delete.connect(VaultSignalHandler.delete_to_vault, sender=model)
|
||||
|
||||
|
||||
class VaultPubSub(LazyObject):
|
||||
def _setup(self):
|
||||
self._wrapped = RedisPubSub('refresh_vault')
|
||||
|
||||
|
||||
vault_pub_sub = VaultPubSub()
|
||||
|
||||
|
||||
@receiver(django_ready)
|
||||
def subscribe_vault_change(sender, **kwargs):
|
||||
logger.debug("Start subscribe vault change")
|
||||
|
||||
vault_pub_sub.subscribe(lambda name: refresh_vault_client())
|
||||
|
|
|
@ -5,6 +5,7 @@ from celery import shared_task
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from accounts.backends import vault_client
|
||||
from accounts.const import VaultTypeChoices
|
||||
from accounts.models import Account, AccountTemplate
|
||||
from common.utils import get_logger
|
||||
from orgs.utils import tmp_to_root_org
|
||||
|
@ -39,6 +40,9 @@ def sync_secret_to_vault():
|
|||
# 这里不能判断 settings.VAULT_ENABLED, 必须判断当前 vault_client 的类型
|
||||
print('\033[35m>>> 当前 Vault 功能未开启, 不需要同步')
|
||||
return
|
||||
if VaultTypeChoices.local == vault_client.type:
|
||||
print('\033[31m>>> 当前第三方 Vault 客户端初始化失败,数据存储在本地数据库')
|
||||
return
|
||||
|
||||
failed, skipped, succeeded = 0, 0, 0
|
||||
to_sync_models = [Account, AccountTemplate, Account.history.model]
|
||||
|
|
|
@ -123,6 +123,10 @@ class AssetViewSet(SuggestionMixin, OrgBulkModelViewSet):
|
|||
NodeFilterBackend, AttrRulesFilterBackend
|
||||
]
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
instance.accounts.update(su_from_id=None)
|
||||
instance.delete()
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
if queryset.model is not Asset:
|
||||
|
|
|
@ -3,6 +3,7 @@ from collections import defaultdict
|
|||
from copy import deepcopy
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.functional import lazy
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from common.db.models import ChoicesMixin
|
||||
|
@ -29,15 +30,15 @@ class AllTypes(ChoicesMixin):
|
|||
|
||||
@classmethod
|
||||
def choices(cls):
|
||||
return lazy(cls.get_choices, list)()
|
||||
|
||||
@classmethod
|
||||
def get_choices(cls):
|
||||
choices = []
|
||||
for tp in cls.includes:
|
||||
choices.extend(tp.get_choices())
|
||||
return choices
|
||||
|
||||
@classmethod
|
||||
def get_choices(cls):
|
||||
return cls.choices()
|
||||
|
||||
@classmethod
|
||||
def filter_choices(cls, category):
|
||||
choices = dict(cls.category_types()).get(category, cls).get_choices()
|
||||
|
|
|
@ -7,6 +7,7 @@ from django.db.models import F, Value, CharField, Q
|
|||
from django.db.models.functions import Cast
|
||||
from django.http import HttpResponse, FileResponse
|
||||
from django.utils.encoding import escape_uri_path
|
||||
from django_celery_beat.models import PeriodicTask
|
||||
from rest_framework import generics
|
||||
from rest_framework import status
|
||||
from rest_framework import viewsets
|
||||
|
@ -22,6 +23,9 @@ from common.plugins.es import QuerySet as ESQuerySet
|
|||
from common.sessions.cache import user_session_manager
|
||||
from common.storage.ftp_file import FTPFileStorageHandler
|
||||
from common.utils import is_uuid, get_logger, lazyproperty
|
||||
from ops.const import Types
|
||||
from ops.models import Job
|
||||
from ops.serializers.job import JobSerializer
|
||||
from orgs.mixins.api import OrgReadonlyModelViewSet, OrgModelViewSet
|
||||
from orgs.models import Organization
|
||||
from orgs.utils import current_org, tmp_to_root_org
|
||||
|
@ -39,14 +43,14 @@ from .serializers import (
|
|||
FTPLogSerializer, UserLoginLogSerializer, JobLogSerializer,
|
||||
OperateLogSerializer, OperateLogActionDetailSerializer,
|
||||
PasswordChangeLogSerializer, ActivityUnionLogSerializer,
|
||||
FileSerializer, UserSessionSerializer
|
||||
FileSerializer, UserSessionSerializer, JobsAuditSerializer
|
||||
)
|
||||
from .utils import construct_userlogin_usernames
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class JobAuditViewSet(OrgReadonlyModelViewSet):
|
||||
class JobLogAuditViewSet(OrgReadonlyModelViewSet):
|
||||
model = JobLog
|
||||
extra_filter_backends = [DatetimeRangeFilterBackend]
|
||||
date_range_filter_fields = [
|
||||
|
@ -58,6 +62,35 @@ class JobAuditViewSet(OrgReadonlyModelViewSet):
|
|||
ordering = ['-date_start']
|
||||
|
||||
|
||||
class JobsAuditViewSet(OrgModelViewSet):
|
||||
model = Job
|
||||
search_fields = ['creator__name']
|
||||
filterset_fields = ['creator__name']
|
||||
serializer_class = JobsAuditSerializer
|
||||
ordering = ['-is_periodic', '-date_updated']
|
||||
http_method_names = ['get', 'options', 'patch']
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.exclude(type=Types.upload_file).filter(instant=False)
|
||||
return queryset
|
||||
|
||||
def perform_update(self, serializer):
|
||||
job = self.get_object()
|
||||
is_periodic = serializer.validated_data.get('is_periodic')
|
||||
if job.is_periodic != is_periodic:
|
||||
job.is_periodic = is_periodic
|
||||
job.save()
|
||||
name, task, args, kwargs = job.get_register_task()
|
||||
task_obj = PeriodicTask.objects.filter(name=name).first()
|
||||
if task_obj:
|
||||
is_periodic = job.is_periodic
|
||||
if task_obj.enabled != is_periodic:
|
||||
task_obj.enabled = is_periodic
|
||||
task_obj.save()
|
||||
return super().perform_update(serializer)
|
||||
|
||||
|
||||
class FTPLogViewSet(OrgModelViewSet):
|
||||
model = FTPLog
|
||||
serializer_class = FTPLogSerializer
|
||||
|
|
|
@ -7,7 +7,7 @@ from audits.backends.db import OperateLogStore
|
|||
from common.serializers.fields import LabeledChoiceField, ObjectRelatedField
|
||||
from common.utils import reverse, i18n_trans
|
||||
from common.utils.timezone import as_current_tz
|
||||
from ops.serializers.job import JobExecutionSerializer
|
||||
from ops.serializers.job import JobExecutionSerializer, JobSerializer
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from terminal.models import Session
|
||||
from users.models import User
|
||||
|
@ -34,6 +34,30 @@ class JobLogSerializer(JobExecutionSerializer):
|
|||
}
|
||||
|
||||
|
||||
class JobsAuditSerializer(JobSerializer):
|
||||
material = serializers.ReadOnlyField(label=_("Command"))
|
||||
summary = serializers.ReadOnlyField(label=_("Summary"))
|
||||
crontab = serializers.ReadOnlyField(label=_("Execution cycle"))
|
||||
is_periodic_display = serializers.BooleanField(read_only=True, source='is_periodic')
|
||||
|
||||
class Meta(JobSerializer.Meta):
|
||||
read_only_fields = [
|
||||
"id", 'name', 'args', 'material', 'type', 'crontab', 'interval', 'date_last_run', 'summary', 'created_by',
|
||||
'is_periodic_display'
|
||||
]
|
||||
fields = read_only_fields + ['is_periodic']
|
||||
|
||||
def validate(self, attrs):
|
||||
allowed_fields = {'is_periodic'}
|
||||
submitted_fields = set(attrs.keys())
|
||||
invalid_fields = submitted_fields - allowed_fields
|
||||
if invalid_fields:
|
||||
raise serializers.ValidationError(
|
||||
f"Updating {', '.join(invalid_fields)} fields is not allowed"
|
||||
)
|
||||
return attrs
|
||||
|
||||
|
||||
class FTPLogSerializer(serializers.ModelSerializer):
|
||||
operate = LabeledChoiceField(choices=OperateChoices.choices, label=_("Operate"))
|
||||
|
||||
|
|
|
@ -187,7 +187,7 @@ def on_django_start_set_operate_log_monitor_models(sender, **kwargs):
|
|||
'PermedAsset', 'PermedAccount', 'MenuPermission',
|
||||
'Permission', 'TicketSession', 'ApplyLoginTicket',
|
||||
'ApplyCommandTicket', 'ApplyLoginAssetTicket',
|
||||
'FavoriteAsset', 'ChangeSecretRecord', 'AppProvider',
|
||||
'FavoriteAsset', 'ChangeSecretRecord', 'AppProvider', 'Variable'
|
||||
}
|
||||
include_models = {'UserSession'}
|
||||
for i, app in enumerate(apps.get_models(), 1):
|
||||
|
|
|
@ -13,7 +13,9 @@ router.register(r'ftp-logs', api.FTPLogViewSet, 'ftp-log')
|
|||
router.register(r'login-logs', api.UserLoginLogViewSet, 'login-log')
|
||||
router.register(r'operate-logs', api.OperateLogViewSet, 'operate-log')
|
||||
router.register(r'password-change-logs', api.PasswordChangeLogViewSet, 'password-change-log')
|
||||
router.register(r'job-logs', api.JobAuditViewSet, 'job-log')
|
||||
router.register(r'job-logs', api.JobLogAuditViewSet, 'job-log')
|
||||
router.register(r'jobs', api.JobsAuditViewSet, 'jobs')
|
||||
|
||||
router.register(r'my-login-logs', api.MyLoginLogViewSet, 'my-login-log')
|
||||
router.register(r'user-sessions', api.UserSessionViewSet, 'user-session')
|
||||
|
||||
|
|
|
@ -1,29 +1,128 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import uuid
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.generics import CreateAPIView
|
||||
from rest_framework.generics import CreateAPIView, RetrieveAPIView
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.exceptions import NotFound
|
||||
|
||||
from common.exceptions import JMSException, UnexpectError
|
||||
from common.permissions import WithBootstrapToken, IsServiceAccount
|
||||
from common.utils import get_logger
|
||||
from users.models.user import User
|
||||
from .. import errors
|
||||
from .. import serializers
|
||||
from ..const import MFA_FACE_CONTEXT_CACHE_KEY_PREFIX, MFA_FACE_SESSION_KEY, MFA_FACE_CONTEXT_CACHE_TTL
|
||||
from ..errors import SessionEmptyError
|
||||
from ..mixins import AuthMixin
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
__all__ = [
|
||||
'MFAChallengeVerifyApi', 'MFASendCodeApi'
|
||||
'MFAChallengeVerifyApi', 'MFASendCodeApi',
|
||||
'MFAFaceCallbackApi', 'MFAFaceContextApi'
|
||||
]
|
||||
|
||||
|
||||
class MFAFaceCallbackApi(AuthMixin, CreateAPIView):
|
||||
permission_classes = (IsServiceAccount,)
|
||||
serializer_class = serializers.MFAFaceCallbackSerializer
|
||||
|
||||
def perform_create(self, serializer):
|
||||
token = serializer.validated_data.get('token')
|
||||
context = self._get_context_from_cache(token)
|
||||
|
||||
if not serializer.validated_data.get('success', False):
|
||||
self._update_context_with_error(
|
||||
context,
|
||||
serializer.validated_data.get('error_message', 'Unknown error')
|
||||
)
|
||||
return Response(status=200)
|
||||
|
||||
face_code = serializer.validated_data.get('face_code')
|
||||
if not face_code:
|
||||
self._update_context_with_error(context, "missing field 'face_code'")
|
||||
raise ValidationError({'error': "missing field 'face_code'"})
|
||||
|
||||
self._handle_success(context, face_code)
|
||||
return Response(status=200)
|
||||
|
||||
@staticmethod
|
||||
def get_face_cache_key(token):
|
||||
return f"{MFA_FACE_CONTEXT_CACHE_KEY_PREFIX}_{token}"
|
||||
|
||||
def _get_context_from_cache(self, token):
|
||||
cache_key = self.get_face_cache_key(token)
|
||||
context = cache.get(cache_key)
|
||||
if not context:
|
||||
raise ValidationError({'error': "token not exists or expired"})
|
||||
return context
|
||||
|
||||
def _update_context_with_error(self, context, error_message):
|
||||
context.update({
|
||||
'is_finished': True,
|
||||
'success': False,
|
||||
'error_message': error_message,
|
||||
})
|
||||
self._update_cache(context)
|
||||
|
||||
def _update_cache(self, context):
|
||||
cache_key = self.get_face_cache_key(context['token'])
|
||||
cache.set(cache_key, context, MFA_FACE_CONTEXT_CACHE_TTL)
|
||||
|
||||
def _handle_success(self, context, face_code):
|
||||
context.update({
|
||||
'is_finished': True,
|
||||
'success': True,
|
||||
'face_code': face_code
|
||||
})
|
||||
self._update_cache(context)
|
||||
|
||||
|
||||
class MFAFaceContextApi(AuthMixin, RetrieveAPIView, CreateAPIView):
|
||||
permission_classes = (AllowAny,)
|
||||
face_token_session_key = MFA_FACE_SESSION_KEY
|
||||
|
||||
@staticmethod
|
||||
def get_face_cache_key(token):
|
||||
return f"{MFA_FACE_CONTEXT_CACHE_KEY_PREFIX}_{token}"
|
||||
|
||||
def new_face_context(self):
|
||||
token = uuid.uuid4().hex
|
||||
cache_key = self.get_face_cache_key(token)
|
||||
face_context = {
|
||||
"token": token,
|
||||
"is_finished": False
|
||||
}
|
||||
cache.set(cache_key, face_context, MFA_FACE_CONTEXT_CACHE_TTL)
|
||||
self.request.session[self.face_token_session_key] = token
|
||||
return token
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
token = self.new_face_context()
|
||||
return Response({'token': token})
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
token = self.request.session.get('mfa_face_token')
|
||||
|
||||
cache_key = self.get_face_cache_key(token)
|
||||
context = cache.get(cache_key)
|
||||
if not context:
|
||||
raise NotFound({'error': "Token does not exist or has expired."})
|
||||
|
||||
return Response({
|
||||
"is_finished": context.get('is_finished', False),
|
||||
"success": context.get('success', False),
|
||||
"error_message": context.get("error_message", '')
|
||||
})
|
||||
|
||||
|
||||
# MFASelectAPi 原来的名字
|
||||
class MFASendCodeApi(AuthMixin, CreateAPIView):
|
||||
"""
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import reverse
|
||||
|
@ -40,12 +41,15 @@ class UserResetPasswordSendCodeApi(CreateAPIView):
|
|||
return user, None
|
||||
|
||||
@staticmethod
|
||||
def safe_send_code(token, code, target, form_type, content):
|
||||
def safe_send_code(token, code, target, form_type, content, user_info):
|
||||
token_sent_key = '{}_send_at'.format(token)
|
||||
token_send_at = cache.get(token_sent_key, 0)
|
||||
if token_send_at:
|
||||
raise IntervalTooShort(60)
|
||||
SendAndVerifyCodeUtil(target, code, backend=form_type, **content).gen_and_send_async()
|
||||
tooler = SendAndVerifyCodeUtil(
|
||||
target, code, backend=form_type, user_info=user_info, **content
|
||||
)
|
||||
tooler.gen_and_send_async()
|
||||
cache.set(token_sent_key, int(time.time()), 60)
|
||||
|
||||
def prepare_code_data(self, user_info, serializer):
|
||||
|
@ -61,7 +65,7 @@ class UserResetPasswordSendCodeApi(CreateAPIView):
|
|||
if not user:
|
||||
raise ValueError(err)
|
||||
|
||||
code = random_string(6, lower=False, upper=False)
|
||||
code = random_string(settings.SMS_CODE_LENGTH, lower=False, upper=False)
|
||||
subject = '%s: %s' % (get_login_title(), _('Forgot password'))
|
||||
context = {
|
||||
'user': user, 'title': subject, 'code': code,
|
||||
|
@ -82,7 +86,7 @@ class UserResetPasswordSendCodeApi(CreateAPIView):
|
|||
code, target, form_type, content = self.prepare_code_data(user_info, serializer)
|
||||
except ValueError as e:
|
||||
return Response({'error': str(e)}, status=400)
|
||||
self.safe_send_code(token, code, target, form_type, content)
|
||||
self.safe_send_code(token, code, target, form_type, content, user_info)
|
||||
return Response({'data': 'ok'}, status=200)
|
||||
|
||||
|
||||
|
|
|
@ -3,8 +3,9 @@
|
|||
import abc
|
||||
import ldap
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
|
||||
from django_auth_ldap.backend import _LDAPUser, LDAPBackend
|
||||
from django_auth_ldap.backend import _LDAPUser, LDAPBackend, valid_cache_key
|
||||
from django_auth_ldap.config import _LDAPConfig, LDAPSearch, LDAPSearchUnion
|
||||
|
||||
from users.utils import construct_user_email
|
||||
|
@ -146,30 +147,53 @@ class LDAPHAAuthorizationBackend(JMSBaseAuthBackend, LDAPBaseBackend):
|
|||
|
||||
class LDAPUser(_LDAPUser):
|
||||
|
||||
def __init__(self, backend, username=None, user=None, request=None):
|
||||
super().__init__(backend=backend, username=username, user=user, request=request)
|
||||
config_prefix = "" if isinstance(self.backend, LDAPAuthorizationBackend) else "_ha"
|
||||
self.user_dn_cache_key = valid_cache_key(
|
||||
f"django_auth_ldap{config_prefix}.user_dn.{self._username}"
|
||||
)
|
||||
self.category = f"ldap{config_prefix}"
|
||||
self.search_filter = getattr(settings, f"AUTH_LDAP{config_prefix.upper()}_SEARCH_FILTER", None)
|
||||
self.search_ou = getattr(settings, f"AUTH_LDAP{config_prefix.upper()}_SEARCH_OU", None)
|
||||
|
||||
def _search_for_user_dn_from_ldap_util(self):
|
||||
from settings.utils import LDAPServerUtil
|
||||
util = LDAPServerUtil()
|
||||
util = LDAPServerUtil(category=self.category)
|
||||
user_dn = util.search_for_user_dn(self._username)
|
||||
return user_dn
|
||||
|
||||
def _load_user_dn(self):
|
||||
"""
|
||||
Populates self._user_dn with the distinguished name of our user.
|
||||
|
||||
This will either construct the DN from a template in
|
||||
AUTH_LDAP_USER_DN_TEMPLATE or connect to the server and search for it.
|
||||
If we have to search, we'll cache the DN.
|
||||
|
||||
"""
|
||||
if self._using_simple_bind_mode():
|
||||
self._user_dn = self._construct_simple_user_dn()
|
||||
else:
|
||||
if self.settings.CACHE_TIMEOUT > 0:
|
||||
self._user_dn = cache.get_or_set(
|
||||
self.user_dn_cache_key, self._search_for_user_dn, self.settings.CACHE_TIMEOUT
|
||||
)
|
||||
else:
|
||||
self._user_dn = self._search_for_user_dn()
|
||||
|
||||
def _search_for_user_dn(self):
|
||||
"""
|
||||
This method was overridden because the AUTH_LDAP_USER_SEARCH
|
||||
configuration in the settings.py file
|
||||
is configured with a `lambda` problem value
|
||||
"""
|
||||
if isinstance(self.backend, LDAPAuthorizationBackend):
|
||||
search_filter = settings.AUTH_LDAP_SEARCH_FILTER
|
||||
search_ou = settings.AUTH_LDAP_SEARCH_OU
|
||||
else:
|
||||
search_filter = settings.AUTH_LDAP_HA_SEARCH_FILTER
|
||||
search_ou = settings.AUTH_LDAP_HA_SEARCH_OU
|
||||
user_search_union = [
|
||||
LDAPSearch(
|
||||
USER_SEARCH, ldap.SCOPE_SUBTREE,
|
||||
search_filter
|
||||
self.search_filter
|
||||
)
|
||||
for USER_SEARCH in str(search_ou).split("|")
|
||||
for USER_SEARCH in str(self.search_ou).split("|")
|
||||
]
|
||||
|
||||
search = LDAPSearchUnion(*user_search_union)
|
||||
|
|
|
@ -22,5 +22,6 @@ class ConfirmMFA(BaseConfirm):
|
|||
|
||||
def authenticate(self, secret_key, mfa_type):
|
||||
mfa_backend = self.user.get_mfa_backend_by_type(mfa_type)
|
||||
mfa_backend.set_request(self.request)
|
||||
ok, msg = mfa_backend.check_code(secret_key)
|
||||
return ok, msg
|
||||
|
|
|
@ -2,7 +2,7 @@ from django.db.models import TextChoices
|
|||
|
||||
from authentication.confirm import CONFIRM_BACKENDS
|
||||
from .confirm import ConfirmMFA, ConfirmPassword, ConfirmReLogin
|
||||
from .mfa import MFAOtp, MFASms, MFARadius, MFACustom
|
||||
from .mfa import MFAOtp, MFASms, MFARadius, MFAFace, MFACustom
|
||||
|
||||
RSA_PRIVATE_KEY = 'rsa_private_key'
|
||||
RSA_PUBLIC_KEY = 'rsa_public_key'
|
||||
|
@ -35,5 +35,11 @@ class ConfirmType(TextChoices):
|
|||
class MFAType(TextChoices):
|
||||
OTP = MFAOtp.name, MFAOtp.display_name
|
||||
SMS = MFASms.name, MFASms.display_name
|
||||
Face = MFAFace.name, MFAFace.display_name
|
||||
Radius = MFARadius.name, MFARadius.display_name
|
||||
Custom = MFACustom.name, MFACustom.display_name
|
||||
|
||||
|
||||
MFA_FACE_CONTEXT_CACHE_KEY_PREFIX = "MFA_FACE_RECOGNITION_CONTEXT"
|
||||
MFA_FACE_CONTEXT_CACHE_TTL = 60
|
||||
MFA_FACE_SESSION_KEY = "mfa_face_token"
|
||||
|
|
|
@ -2,3 +2,4 @@ from .otp import MFAOtp, otp_failed_msg
|
|||
from .sms import MFASms
|
||||
from .radius import MFARadius
|
||||
from .custom import MFACustom
|
||||
from .face import MFAFace
|
|
@ -12,10 +12,14 @@ class BaseMFA(abc.ABC):
|
|||
因为首页登录时,可能没法获取到一些状态
|
||||
"""
|
||||
self.user = user
|
||||
self.request = None
|
||||
|
||||
def is_authenticated(self):
|
||||
return self.user and self.user.is_authenticated
|
||||
|
||||
def set_request(self, request):
|
||||
self.request = request
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def name(self):
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
from django.core.cache import cache
|
||||
|
||||
from authentication.mfa.base import BaseMFA
|
||||
from django.shortcuts import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from authentication.mixins import MFAFaceMixin
|
||||
from common.const import LicenseEditionChoices
|
||||
from settings.api import settings
|
||||
|
||||
|
||||
class MFAFace(BaseMFA, MFAFaceMixin):
|
||||
name = "face"
|
||||
display_name = _('Face Recognition')
|
||||
placeholder = 'Face Recognition'
|
||||
|
||||
def check_code(self, code):
|
||||
|
||||
assert self.is_authenticated()
|
||||
|
||||
try:
|
||||
code = self.get_face_code()
|
||||
if not self.user.check_face(code):
|
||||
return False, _('Facial comparison failed')
|
||||
except Exception as e:
|
||||
return False, "{}:{}".format(_('Facial comparison failed'), str(e))
|
||||
return True, ''
|
||||
|
||||
def is_active(self):
|
||||
if not self.is_authenticated():
|
||||
return True
|
||||
return bool(self.user.face_vector)
|
||||
|
||||
@staticmethod
|
||||
def global_enabled():
|
||||
return settings.XPACK_LICENSE_IS_VALID \
|
||||
and LicenseEditionChoices.ULTIMATE == \
|
||||
LicenseEditionChoices.from_key(settings.XPACK_LICENSE_EDITION) \
|
||||
and settings.FACE_RECOGNITION_ENABLED
|
||||
|
||||
def get_enable_url(self) -> str:
|
||||
return reverse('authentication:user-face-enable')
|
||||
|
||||
def get_disable_url(self) -> str:
|
||||
return reverse('authentication:user-face-disable')
|
||||
|
||||
def disable(self):
|
||||
assert self.is_authenticated()
|
||||
self.user.face_vector = ''
|
||||
self.user.save(update_fields=['face_vector'])
|
||||
|
||||
def can_disable(self) -> bool:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def help_text_of_enable():
|
||||
return _("Frontal Face Recognition")
|
|
@ -12,7 +12,7 @@ class MFARadius(BaseMFA):
|
|||
display_name = 'Radius'
|
||||
placeholder = _("Radius verification code")
|
||||
|
||||
def check_code(self, code):
|
||||
def check_code(self, code=None):
|
||||
assert self.is_authenticated()
|
||||
backend = RadiusBackend()
|
||||
username = self.user.username
|
||||
|
|
|
@ -2,6 +2,7 @@ from django.conf import settings
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.utils.verify_code import SendAndVerifyCodeUtil
|
||||
from users.serializers import SmsUserSerializer
|
||||
from .base import BaseMFA
|
||||
|
||||
sms_failed_msg = _("SMS verify code invalid")
|
||||
|
@ -14,8 +15,13 @@ class MFASms(BaseMFA):
|
|||
|
||||
def __init__(self, user):
|
||||
super().__init__(user)
|
||||
phone = user.phone if self.is_authenticated() else ''
|
||||
self.sms = SendAndVerifyCodeUtil(phone, backend=self.name)
|
||||
phone, user_info = '', None
|
||||
if self.is_authenticated():
|
||||
phone = user.phone
|
||||
user_info = SmsUserSerializer(user).data
|
||||
self.sms = SendAndVerifyCodeUtil(
|
||||
phone, backend=self.name, user_info=user_info
|
||||
)
|
||||
|
||||
def check_code(self, code):
|
||||
assert self.is_authenticated()
|
||||
|
|
|
@ -35,7 +35,7 @@ class MFAMiddleware:
|
|||
|
||||
# 这个是 mfa 登录页需要的请求, 也得放出来, 用户其实已经在 CAS/OIDC 中完成登录了
|
||||
white_urls = [
|
||||
'login/mfa', 'mfa/select', 'jsi18n/', '/static/',
|
||||
'login/mfa', 'mfa/select', 'mfa/face','jsi18n/', '/static/',
|
||||
'/profile/otp', '/logout/',
|
||||
]
|
||||
for url in white_urls:
|
||||
|
|
|
@ -199,6 +199,53 @@ class AuthPreCheckMixin:
|
|||
self.raise_credential_error(errors.reason_user_not_exist)
|
||||
|
||||
|
||||
class MFAFaceMixin:
|
||||
request = None
|
||||
|
||||
def get_face_recognition_token(self):
|
||||
from authentication.const import MFA_FACE_SESSION_KEY
|
||||
token = self.request.session.get(MFA_FACE_SESSION_KEY)
|
||||
if not token:
|
||||
raise ValueError("Face recognition token is missing from the session.")
|
||||
return token
|
||||
|
||||
@staticmethod
|
||||
def get_face_cache_key(token):
|
||||
from authentication.const import MFA_FACE_CONTEXT_CACHE_KEY_PREFIX
|
||||
return f"{MFA_FACE_CONTEXT_CACHE_KEY_PREFIX}_{token}"
|
||||
|
||||
def get_face_recognition_context(self):
|
||||
token = self.get_face_recognition_token()
|
||||
cache_key = self.get_face_cache_key(token)
|
||||
context = cache.get(cache_key)
|
||||
if not context:
|
||||
raise ValueError(f"Face recognition context does not exist for token: {token}")
|
||||
return context
|
||||
|
||||
@staticmethod
|
||||
def is_context_finished(context):
|
||||
return context.get('is_finished', False)
|
||||
|
||||
@staticmethod
|
||||
def is_context_success(context):
|
||||
return context.get('success', False)
|
||||
|
||||
def get_face_code(self):
|
||||
context = self.get_face_recognition_context()
|
||||
|
||||
if not self.is_context_finished(context):
|
||||
raise RuntimeError("Face recognition is not yet completed.")
|
||||
|
||||
if not self.is_context_success(context):
|
||||
msg = context.get('error_message', '')
|
||||
raise RuntimeError(msg)
|
||||
|
||||
face_code = context.get('face_code')
|
||||
if not face_code:
|
||||
raise ValueError("Face code is missing from the context.")
|
||||
return face_code
|
||||
|
||||
|
||||
class MFAMixin:
|
||||
request: Request
|
||||
get_user_from_session: Callable
|
||||
|
@ -263,7 +310,6 @@ class MFAMixin:
|
|||
user = user if user else self.get_user_from_session()
|
||||
if not user.mfa_enabled:
|
||||
return
|
||||
|
||||
# 监测 MFA 是不是屏蔽了
|
||||
ip = self.get_request_ip()
|
||||
self.check_mfa_is_block(user.username, ip)
|
||||
|
@ -276,6 +322,7 @@ class MFAMixin:
|
|||
elif not mfa_backend.is_active():
|
||||
msg = backend_error.format(mfa_backend.display_name)
|
||||
else:
|
||||
mfa_backend.set_request(self.request)
|
||||
ok, msg = mfa_backend.check_code(code)
|
||||
|
||||
if ok:
|
||||
|
|
|
@ -8,6 +8,7 @@ from common.serializers.fields import EncryptedField
|
|||
__all__ = [
|
||||
'MFAChallengeSerializer', 'MFASelectTypeSerializer',
|
||||
'PasswordVerifySerializer', 'ResetPasswordCodeSerializer',
|
||||
'MFAFaceCallbackSerializer'
|
||||
]
|
||||
|
||||
|
||||
|
@ -51,3 +52,16 @@ class MFAChallengeSerializer(serializers.Serializer):
|
|||
|
||||
def update(self, instance, validated_data):
|
||||
pass
|
||||
|
||||
|
||||
class MFAFaceCallbackSerializer(serializers.Serializer):
|
||||
token = serializers.CharField(required=True, allow_blank=False)
|
||||
success = serializers.BooleanField(required=True, allow_null=False)
|
||||
error_message = serializers.CharField(required=False, allow_null=True, allow_blank=True)
|
||||
face_code = serializers.CharField(required=False, allow_null=True, allow_blank=True)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
pass
|
||||
|
||||
def create(self, validated_data):
|
||||
pass
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
{% extends '_base_only_content.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if 'code' in form.errors %}
|
||||
<div class="alert alert-danger" id="messages">
|
||||
<p class="red-fonts">{{ form.code.errors.as_text }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="retry_container" style="text-align: center; margin-top: 20px; display: none;">
|
||||
<button id="retry_button" class="btn btn-primary">{% trans 'Retry' %}</button>
|
||||
</div>
|
||||
|
||||
<form class="m-t" role="form" method="post" action="" style="display: none">
|
||||
{% csrf_token %}
|
||||
<button id="submit_button" type="submit" style="display: none"></button>
|
||||
</form>
|
||||
|
||||
<div id="iframe_container"
|
||||
style="display: none; justify-content: center; align-items: center; height: 520px; width: 100%;">
|
||||
<iframe
|
||||
title="face capture"
|
||||
id="face_capture_iframe"
|
||||
allow="camera"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
style="width: 100%; height:100%;border: none;">
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
const apiUrl = "{% url 'api-auth:mfa-face-context' %}";
|
||||
const faceCaptureUrl = "/facelive/capture";
|
||||
let token;
|
||||
|
||||
function createFaceCaptureToken() {
|
||||
const csrf = getCookie('jms_csrftoken');
|
||||
$.ajax({
|
||||
url: apiUrl,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': csrf
|
||||
},
|
||||
success: function (data) {
|
||||
token = data.token;
|
||||
$('#iframe_container').show();
|
||||
$('#face_capture_iframe').attr('src', `${faceCaptureUrl}?token=${token}`);
|
||||
startCheckingStatus();
|
||||
},
|
||||
error: function (error) {
|
||||
$('#retry_container').show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function startCheckingStatus() {
|
||||
const interval = 1000;
|
||||
const timer = setInterval(function () {
|
||||
$.ajax({
|
||||
url: `${apiUrl}?token=${token}`,
|
||||
method: 'GET',
|
||||
success: function (data) {
|
||||
if (data.is_finished) {
|
||||
clearInterval(timer);
|
||||
$('#submit_button').click();
|
||||
}
|
||||
},
|
||||
error: function (error) {
|
||||
console.error('API request failed:', error);
|
||||
}
|
||||
});
|
||||
}, interval);
|
||||
}
|
||||
|
||||
const active = "{{ active }}";
|
||||
if (active) {
|
||||
createFaceCaptureToken();
|
||||
} else {
|
||||
$('#retry_container').show();
|
||||
}
|
||||
|
||||
$('#retry_button').on('click', function () {
|
||||
window.location.href = "{% url 'authentication:login-face-capture' %}";
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -33,6 +33,8 @@ urlpatterns = [
|
|||
path('mfa/challenge/', api.MFAChallengeVerifyApi.as_view(), name='mfa-challenge'),
|
||||
path('mfa/select/', api.MFASendCodeApi.as_view(), name='mfa-select'),
|
||||
path('mfa/send-code/', api.MFASendCodeApi.as_view(), name='mfa-send-code'),
|
||||
path('mfa/face/callback/', api.MFAFaceCallbackApi.as_view(), name='mfa-face-callback'),
|
||||
path('mfa/face/context/', api.MFAFaceContextApi.as_view(), name='mfa-face-context'),
|
||||
path('password/reset-code/', api.UserResetPasswordSendCodeApi.as_view(), name='reset-password-code'),
|
||||
path('password/verify/', api.UserPasswordVerifyApi.as_view(), name='user-password-verify'),
|
||||
path('login-confirm-ticket/status/', api.TicketStatusApi.as_view(), name='login-confirm-ticket-status'),
|
||||
|
|
|
@ -14,6 +14,7 @@ urlpatterns = [
|
|||
path('login/', non_atomic_requests(views.UserLoginView.as_view()), name='login'),
|
||||
path('login/mfa/', views.UserLoginMFAView.as_view(), name='login-mfa'),
|
||||
path('login/wait-confirm/', views.UserLoginWaitConfirmView.as_view(), name='login-wait-confirm'),
|
||||
path('login/mfa/face/capture/', views.UserLoginMFAFaceView.as_view(), name='login-face-capture'),
|
||||
path('login/guard/', views.UserLoginGuardView.as_view(), name='login-guard'),
|
||||
path('logout/', views.UserLogoutView.as_view(), name='logout'),
|
||||
|
||||
|
@ -73,6 +74,8 @@ urlpatterns = [
|
|||
path('profile/otp/disable/', users_view.UserOtpDisableView.as_view(),
|
||||
name='user-otp-disable'),
|
||||
|
||||
path('profile/face/enable/', users_view.UserFaceEnableView.as_view(), name='user-face-enable'),
|
||||
path('profile/face/disable/', users_view.UserFaceDisableView.as_view(), name='user-face-disable'),
|
||||
# other authentication protocol
|
||||
path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')),
|
||||
path('openid/', include(('authentication.backends.oidc.urls', 'authentication'), namespace='openid')),
|
||||
|
|
|
@ -24,8 +24,8 @@ from django.views.decorators.debug import sensitive_post_parameters
|
|||
from django.views.generic.base import TemplateView, RedirectView
|
||||
from django.views.generic.edit import FormView
|
||||
|
||||
from common.utils import FlashMessageUtil, static_or_direct, safe_next_url
|
||||
from common.const import Language
|
||||
from common.utils import FlashMessageUtil, static_or_direct, safe_next_url
|
||||
from users.utils import (
|
||||
redirect_user_first_login_or_index
|
||||
)
|
||||
|
|
|
@ -3,14 +3,16 @@
|
|||
|
||||
from __future__ import unicode_literals
|
||||
from django.views.generic.edit import FormView
|
||||
from django.shortcuts import redirect
|
||||
from django.shortcuts import redirect, reverse
|
||||
|
||||
from common.utils import get_logger
|
||||
from users.views import UserFaceCaptureView
|
||||
from .. import forms, errors, mixins
|
||||
from .utils import redirect_to_guard_view
|
||||
from ..const import MFAType
|
||||
|
||||
logger = get_logger(__name__)
|
||||
__all__ = ['UserLoginMFAView']
|
||||
__all__ = ['UserLoginMFAView', 'UserLoginMFAFaceView']
|
||||
|
||||
|
||||
class UserLoginMFAView(mixins.AuthMixin, FormView):
|
||||
|
@ -32,10 +34,16 @@ class UserLoginMFAView(mixins.AuthMixin, FormView):
|
|||
return super().get(*args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
from users.utils import MFABlockUtils
|
||||
code = form.cleaned_data.get('code')
|
||||
mfa_type = form.cleaned_data.get('mfa_type')
|
||||
|
||||
if mfa_type == MFAType.Face:
|
||||
return redirect(reverse('authentication:login-face-capture'))
|
||||
return self.do_mfa_check(form, code, mfa_type)
|
||||
|
||||
def do_mfa_check(self, form, code, mfa_type):
|
||||
from users.utils import MFABlockUtils
|
||||
|
||||
try:
|
||||
self._do_check_user_mfa(code, mfa_type)
|
||||
user, ip = self.get_user_from_session(), self.get_request_ip()
|
||||
|
@ -58,3 +66,7 @@ class UserLoginMFAView(mixins.AuthMixin, FormView):
|
|||
kwargs.update(mfa_context)
|
||||
return kwargs
|
||||
|
||||
|
||||
class UserLoginMFAFaceView(UserFaceCaptureView, UserLoginMFAView):
|
||||
def form_valid(self, form):
|
||||
return self.do_mfa_check(form, self.code, self.mfa_type)
|
||||
|
|
|
@ -13,20 +13,17 @@ from authentication.const import ConfirmType
|
|||
from authentication.mixins import AuthMixin
|
||||
from authentication.permissions import UserConfirmation
|
||||
from common.sdk.im.wecom import URL
|
||||
from common.sdk.im.wecom import WeCom
|
||||
from common.sdk.im.wecom import WeCom, wecom_tool
|
||||
from common.utils import get_logger
|
||||
from common.utils.common import get_request_ip
|
||||
from common.utils.django import reverse, get_object_or_none, safe_next_url
|
||||
from common.utils.random import random_string
|
||||
from common.views.mixins import UserConfirmRequiredExceptionMixin, PermissionsMixin
|
||||
from users.models import User
|
||||
from users.views import UserVerifyPasswordView
|
||||
from .base import BaseLoginCallbackView, BaseBindCallbackView
|
||||
from .mixins import METAMixin, FlashMessageMixin
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
WECOM_STATE_SESSION_KEY = '_wecom_state'
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class WeComBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashMessageMixin, View):
|
||||
|
@ -45,7 +42,7 @@ class WeComBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashM
|
|||
)
|
||||
|
||||
def verify_state(self):
|
||||
return self.verify_state_with_session_key(WECOM_STATE_SESSION_KEY)
|
||||
return wecom_tool.check_state(self.request.GET.get('state'), self.request)
|
||||
|
||||
def get_already_bound_response(self, redirect_url):
|
||||
msg = _('WeCom is already bound')
|
||||
|
@ -56,13 +53,10 @@ class WeComBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashM
|
|||
class WeComQRMixin(WeComBaseMixin, View):
|
||||
|
||||
def get_qr_url(self, redirect_uri):
|
||||
state = random_string(16)
|
||||
self.request.session[WECOM_STATE_SESSION_KEY] = state
|
||||
|
||||
params = {
|
||||
'appid': settings.WECOM_CORPID,
|
||||
'agentid': settings.WECOM_AGENTID,
|
||||
'state': state,
|
||||
'state': wecom_tool.gen_state(request=self.request),
|
||||
'redirect_uri': redirect_uri,
|
||||
}
|
||||
url = URL.QR_CONNECT + '?' + urlencode(params)
|
||||
|
@ -74,13 +68,11 @@ class WeComOAuthMixin(WeComBaseMixin, View):
|
|||
def get_oauth_url(self, redirect_uri):
|
||||
if not settings.AUTH_WECOM:
|
||||
return reverse('authentication:login')
|
||||
state = random_string(16)
|
||||
self.request.session[WECOM_STATE_SESSION_KEY] = state
|
||||
|
||||
params = {
|
||||
'appid': settings.WECOM_CORPID,
|
||||
'agentid': settings.WECOM_AGENTID,
|
||||
'state': state,
|
||||
'state': wecom_tool.gen_state(request=self.request),
|
||||
'redirect_uri': redirect_uri,
|
||||
'response_type': 'code',
|
||||
'scope': 'snsapi_base',
|
||||
|
|
|
@ -76,3 +76,32 @@ class Language(models.TextChoices):
|
|||
|
||||
|
||||
COUNTRY_CALLING_CODES = get_country_phone_choices()
|
||||
|
||||
|
||||
class LicenseEditionChoices(models.TextChoices):
|
||||
COMMUNITY = 'community', _('Community edition')
|
||||
BASIC = 'basic', _('Basic edition')
|
||||
STANDARD = 'standard', _('Standard edition')
|
||||
PROFESSIONAL = 'professional', _('Professional edition')
|
||||
ULTIMATE = 'ultimate', _('Ultimate edition')
|
||||
|
||||
@staticmethod
|
||||
def from_key(key: str):
|
||||
for choice in LicenseEditionChoices:
|
||||
if choice == key:
|
||||
return choice
|
||||
return LicenseEditionChoices.COMMUNITY
|
||||
@staticmethod
|
||||
def parse_license_edition(info):
|
||||
count = info.get('license', {}).get('count', 0)
|
||||
|
||||
if 50 >= count > 0:
|
||||
return LicenseEditionChoices.BASIC
|
||||
elif count <= 500:
|
||||
return LicenseEditionChoices.STANDARD
|
||||
elif count < 5000:
|
||||
return LicenseEditionChoices.PROFESSIONAL
|
||||
elif count >= 5000:
|
||||
return LicenseEditionChoices.ULTIMATE
|
||||
else:
|
||||
return LicenseEditionChoices.COMMUNITY
|
||||
|
|
|
@ -126,7 +126,7 @@ class BaseFileParser(BaseParser):
|
|||
value = self.id_name_to_obj(value)
|
||||
elif isinstance(field, LabeledChoiceField):
|
||||
value = self.id_name_to_obj(value)
|
||||
if isinstance(value, dict) and value.get('pk'):
|
||||
if isinstance(value, dict) and 'pk' in value:
|
||||
value = value.get('pk')
|
||||
elif isinstance(field, serializers.ListSerializer):
|
||||
value = [self.parse_value(field.child, v) for v in value]
|
||||
|
|
|
@ -73,6 +73,7 @@ known_unauth_urls = [
|
|||
"/api/v1/authentication/password/reset-code/",
|
||||
"/api/v1/authentication/login-confirm-ticket/status/",
|
||||
"/api/v1/authentication/mfa/select/",
|
||||
"/api/v1/authentication/mfa/face/context/",
|
||||
"/api/v1/authentication/mfa/send-code/",
|
||||
"/api/v1/authentication/sso/login/",
|
||||
"/api/v1/authentication/user-session/",
|
||||
|
|
|
@ -16,12 +16,6 @@ def digest(corp_id, corp_secret):
|
|||
return dist
|
||||
|
||||
|
||||
def update_values(default: dict, others: dict):
|
||||
for key in default.keys():
|
||||
if key in others:
|
||||
default[key] = others[key]
|
||||
|
||||
|
||||
def set_default(data: dict, default: dict):
|
||||
for key in default.keys():
|
||||
if key not in data:
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
from typing import Iterable, AnyStr
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
from common.sdk.im.mixin import RequestMixin, BaseRequest
|
||||
from common.sdk.im.utils import digest, update_values
|
||||
from common.utils.common import get_logger
|
||||
from common.sdk.im.utils import digest
|
||||
from common.utils import reverse, random_string, get_logger, lazyproperty
|
||||
from users.utils import construct_user_email, flatten_dict, map_attributes
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
@ -107,15 +109,6 @@ class WeCom(RequestMixin):
|
|||
对于业务代码,只需要关心由 用户id 或 消息不对 导致的错误,其他错误不予理会
|
||||
"""
|
||||
users = tuple(users)
|
||||
|
||||
extra_params = {
|
||||
"safe": 0,
|
||||
"enable_id_trans": 0,
|
||||
"enable_duplicate_check": 0,
|
||||
"duplicate_check_interval": 1800
|
||||
}
|
||||
update_values(extra_params, kwargs)
|
||||
|
||||
body = {
|
||||
"touser": '|'.join(users),
|
||||
"msgtype": "text",
|
||||
|
@ -123,7 +116,7 @@ class WeCom(RequestMixin):
|
|||
"text": {
|
||||
"content": msg
|
||||
},
|
||||
**extra_params
|
||||
**kwargs
|
||||
}
|
||||
if markdown:
|
||||
body['msgtype'] = 'markdown'
|
||||
|
@ -144,15 +137,15 @@ class WeCom(RequestMixin):
|
|||
if 'invaliduser' not in data:
|
||||
return ()
|
||||
|
||||
invaliduser = data['invaliduser']
|
||||
if not invaliduser:
|
||||
invalid_user = data['invaliduser']
|
||||
if not invalid_user:
|
||||
return ()
|
||||
|
||||
if isinstance(invaliduser, str):
|
||||
logger.error(f'WeCom send text 200, but invaliduser is not str: invaliduser={invaliduser}')
|
||||
if isinstance(invalid_user, str):
|
||||
logger.error(f'WeCom send text 200, but invaliduser is not str: invaliduser={invalid_user}')
|
||||
raise WeComError
|
||||
|
||||
invalid_users = invaliduser.split('|')
|
||||
invalid_users = invalid_user.split('|')
|
||||
return invalid_users
|
||||
|
||||
def get_user_id_by_code(self, code):
|
||||
|
@ -167,13 +160,12 @@ class WeCom(RequestMixin):
|
|||
|
||||
self._requests.check_errcode_is_0(data)
|
||||
|
||||
USER_ID = 'UserId'
|
||||
OPEN_ID = 'OpenId'
|
||||
|
||||
if USER_ID in data:
|
||||
return data[USER_ID], USER_ID
|
||||
elif OPEN_ID in data:
|
||||
return data[OPEN_ID], OPEN_ID
|
||||
user_id = 'UserId'
|
||||
open_id = 'OpenId'
|
||||
if user_id in data:
|
||||
return data[user_id], user_id
|
||||
elif open_id in data:
|
||||
return data[open_id], open_id
|
||||
else:
|
||||
logger.error(f'WeCom response 200 but get field from json error: fields=UserId|OpenId')
|
||||
raise WeComError
|
||||
|
@ -195,3 +187,37 @@ class WeCom(RequestMixin):
|
|||
default_detail = self.default_user_detail(data, user_id)
|
||||
detail = map_attributes(default_detail, info, self.attributes)
|
||||
return detail
|
||||
|
||||
|
||||
class WeComTool(object):
|
||||
WECOM_STATE_SESSION_KEY = '_wecom_state'
|
||||
WECOM_STATE_VALUE = 'wecom'
|
||||
|
||||
@lazyproperty
|
||||
def qr_cb_url(self):
|
||||
return reverse('authentication:wecom-qr-login-callback', external=True)
|
||||
|
||||
def gen_state(self, request=None):
|
||||
state = random_string(16)
|
||||
if not request:
|
||||
cache.set(state, self.WECOM_STATE_VALUE, timeout=60 * 60 * 24)
|
||||
else:
|
||||
request.session[self.WECOM_STATE_SESSION_KEY] = state
|
||||
return state
|
||||
|
||||
def check_state(self, state, request=None):
|
||||
return cache.get(state) == self.WECOM_STATE_VALUE or \
|
||||
request.session[self.WECOM_STATE_SESSION_KEY] == state
|
||||
|
||||
def wrap_redirect_url(self, next_url):
|
||||
params = {
|
||||
'appid': settings.WECOM_CORPID,
|
||||
'agentid': settings.WECOM_AGENTID,
|
||||
'state': self.gen_state(),
|
||||
'redirect_uri': f'{self.qr_cb_url}?next={next_url}',
|
||||
'response_type': 'code', 'scope': 'snsapi_base',
|
||||
}
|
||||
return URL.OAUTH_CONNECT + '?' + urlencode(params) + '#wechat_redirect'
|
||||
|
||||
|
||||
wecom_tool = WeComTool()
|
||||
|
|
|
@ -43,7 +43,7 @@ class SMS:
|
|||
**kwargs
|
||||
)
|
||||
|
||||
def send_verify_code(self, phone_number, code):
|
||||
def send_verify_code(self, phone_number, code, **kwargs):
|
||||
prefix = getattr(self.client, 'SIGN_AND_TMPL_SETTING_FIELD_PREFIX', '')
|
||||
sign_name = getattr(settings, f'{prefix}_VERIFY_SIGN_NAME', None)
|
||||
template_code = getattr(settings, f'{prefix}_VERIFY_TEMPLATE_CODE', None)
|
||||
|
@ -53,4 +53,7 @@ class SMS:
|
|||
code='verify_code_sign_tmpl_invalid',
|
||||
detail=_('SMS verification code signature or template invalid')
|
||||
)
|
||||
return self.send_sms([phone_number], sign_name, template_code, OrderedDict(code=code))
|
||||
return self.send_sms(
|
||||
[phone_number], sign_name, template_code,
|
||||
OrderedDict(code=code), **kwargs
|
||||
)
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import os
|
||||
|
||||
import jms_storage
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import default_storage
|
||||
|
||||
from common.storage import jms_storage
|
||||
from common.utils import get_logger, make_dirs
|
||||
from terminal.models import ReplayStorage
|
||||
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2018
|
||||
#
|
||||
|
||||
__version__ = '0.0.59'
|
||||
|
||||
from .ftp import FTPStorage
|
||||
from .oss import OSSStorage
|
||||
from .obs import OBSStorage
|
||||
from .s3 import S3Storage
|
||||
from .azure import AzureStorage
|
||||
from .ceph import CEPHStorage
|
||||
from .jms import JMSReplayStorage, JMSCommandStorage
|
||||
from .multi import MultiObjectStorage
|
||||
from .sftp import SFTPStorage
|
||||
|
||||
|
||||
def get_object_storage(config):
|
||||
if config.get("TYPE") in ["s3", "ceph", "swift", "cos"]:
|
||||
return S3Storage(config)
|
||||
elif config.get("TYPE") == "oss":
|
||||
return OSSStorage(config)
|
||||
elif config.get("TYPE") == "server":
|
||||
return JMSReplayStorage(config)
|
||||
elif config.get("TYPE") == "azure":
|
||||
return AzureStorage(config)
|
||||
elif config.get("TYPE") == "ceph":
|
||||
return CEPHStorage(config)
|
||||
elif config.get("TYPE") == "ftp":
|
||||
return FTPStorage(config)
|
||||
elif config.get("TYPE") == "obs":
|
||||
return OBSStorage(config)
|
||||
elif config.get("TYPE") == "sftp":
|
||||
return SFTPStorage(config)
|
||||
else:
|
||||
return JMSReplayStorage(config)
|
||||
|
||||
|
||||
def get_multi_object_storage(configs):
|
||||
return MultiObjectStorage(configs)
|
|
@ -0,0 +1,61 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import os
|
||||
|
||||
from azure.storage.blob import BlobServiceClient
|
||||
|
||||
from .base import ObjectStorage
|
||||
|
||||
|
||||
class AzureStorage(ObjectStorage):
|
||||
|
||||
def __init__(self, config):
|
||||
self.account_name = config.get("ACCOUNT_NAME", None)
|
||||
self.account_key = config.get("ACCOUNT_KEY", None)
|
||||
self.container_name = config.get("CONTAINER_NAME", None)
|
||||
self.endpoint_suffix = config.get("ENDPOINT_SUFFIX", 'core.chinacloudapi.cn')
|
||||
|
||||
if self.account_name and self.account_key:
|
||||
self.service_client = BlobServiceClient(
|
||||
account_url=f'https://{self.account_name}.blob.{self.endpoint_suffix}',
|
||||
credential={'account_name': self.account_name, 'account_key': self.account_key}
|
||||
)
|
||||
self.client = self.service_client.get_container_client(self.container_name)
|
||||
else:
|
||||
self.client = None
|
||||
|
||||
def upload(self, src, target):
|
||||
try:
|
||||
self.client.upload_blob(target, src)
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, e
|
||||
|
||||
def download(self, src, target):
|
||||
try:
|
||||
blob_data = self.client.download_blob(blob=src)
|
||||
os.makedirs(os.path.dirname(target), 0o755, exist_ok=True)
|
||||
with open(target, 'wb') as writer:
|
||||
writer.write(blob_data.readall())
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, e
|
||||
|
||||
def delete(self, path):
|
||||
try:
|
||||
self.client.delete_blob(path)
|
||||
return True, False
|
||||
except Exception as e:
|
||||
return False, e
|
||||
|
||||
def exists(self, path):
|
||||
resp = self.client.list_blobs(name_starts_with=path)
|
||||
return len(list(resp)) != 0
|
||||
|
||||
def list_buckets(self):
|
||||
return list(self.service_client.list_containers())
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return 'azure'
|
|
@ -0,0 +1,51 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import abc
|
||||
|
||||
|
||||
class ObjectStorage(metaclass=abc.ABCMeta):
|
||||
@abc.abstractmethod
|
||||
def upload(self, src, target):
|
||||
return None, None
|
||||
|
||||
@abc.abstractmethod
|
||||
def download(self, src, target):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def delete(self, path):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def exists(self, path):
|
||||
pass
|
||||
|
||||
def is_valid(self, src, target):
|
||||
ok, msg = self.upload(src=src, target=target)
|
||||
if not ok:
|
||||
return False
|
||||
self.delete(path=target)
|
||||
return True
|
||||
|
||||
|
||||
class LogStorage(metaclass=abc.ABCMeta):
|
||||
@abc.abstractmethod
|
||||
def save(self, command):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def bulk_save(self, command_set, raise_on_error=True):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def filter(self, date_from=None, date_to=None,
|
||||
user=None, asset=None, account=None,
|
||||
input=None, session=None):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def count(self, date_from=None, date_to=None,
|
||||
user=None, asset=None, account=None,
|
||||
input=None, session=None):
|
||||
pass
|
|
@ -0,0 +1,68 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import os
|
||||
import boto
|
||||
import boto.s3.connection
|
||||
|
||||
from .base import ObjectStorage
|
||||
|
||||
|
||||
class CEPHStorage(ObjectStorage):
|
||||
|
||||
def __init__(self, config):
|
||||
self.bucket = config.get("BUCKET", None)
|
||||
self.region = config.get("REGION", None)
|
||||
self.access_key = config.get("ACCESS_KEY", None)
|
||||
self.secret_key = config.get("SECRET_KEY", None)
|
||||
self.hostname = config.get("HOSTNAME", None)
|
||||
self.port = config.get("PORT", 7480)
|
||||
|
||||
if self.hostname and self.access_key and self.secret_key:
|
||||
self.conn = boto.connect_s3(
|
||||
aws_access_key_id=self.access_key,
|
||||
aws_secret_access_key=self.secret_key,
|
||||
host=self.hostname,
|
||||
port=self.port,
|
||||
is_secure=False,
|
||||
calling_format=boto.s3.connection.OrdinaryCallingFormat(),
|
||||
)
|
||||
|
||||
try:
|
||||
self.client = self.conn.get_bucket(bucket_name=self.bucket)
|
||||
except Exception:
|
||||
self.client = None
|
||||
|
||||
def upload(self, src, target):
|
||||
try:
|
||||
key = self.client.new_key(target)
|
||||
key.set_contents_from_filename(src)
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, e
|
||||
|
||||
def download(self, src, target):
|
||||
try:
|
||||
os.makedirs(os.path.dirname(target), 0o755, exist_ok=True)
|
||||
key = self.client.get_key(src)
|
||||
key.get_contents_to_filename(target)
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, e
|
||||
|
||||
def delete(self, path):
|
||||
try:
|
||||
self.client.delete_key(path)
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, e
|
||||
|
||||
def exists(self, path):
|
||||
try:
|
||||
return self.client.get_key(path)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return 'ceph'
|
|
@ -0,0 +1,116 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import os
|
||||
from ftplib import FTP, error_perm
|
||||
from .base import ObjectStorage
|
||||
|
||||
|
||||
class FTPStorage(ObjectStorage):
|
||||
|
||||
def __init__(self, config):
|
||||
self.host = config.get("HOST", None)
|
||||
self.port = int(config.get("PORT", 21))
|
||||
self.username = config.get("USERNAME", None)
|
||||
self.password = config.get("PASSWORD", None)
|
||||
self.pasv = bool(config.get("PASV", False))
|
||||
self.dir = config.get("DIR", "replay")
|
||||
self.client = FTP()
|
||||
self.client.encoding = 'utf-8'
|
||||
self.client.set_pasv(self.pasv)
|
||||
self.pwd = '.'
|
||||
self.connect()
|
||||
|
||||
def connect(self, timeout=-999, source_address=None):
|
||||
self.client.connect(self.host, self.port, timeout, source_address)
|
||||
self.client.login(self.username, self.password)
|
||||
if not self.check_dir_exist(self.dir):
|
||||
self.mkdir(self.dir)
|
||||
self.client.cwd(self.dir)
|
||||
self.pwd = self.client.pwd()
|
||||
|
||||
def confirm_connected(self):
|
||||
try:
|
||||
self.client.pwd()
|
||||
except Exception:
|
||||
self.connect()
|
||||
|
||||
def upload(self, src, target):
|
||||
self.confirm_connected()
|
||||
target_dir = os.path.dirname(target)
|
||||
exist = self.check_dir_exist(target_dir)
|
||||
if not exist:
|
||||
ok = self.mkdir(target_dir)
|
||||
if not ok:
|
||||
raise PermissionError('Dir create error: %s' % target)
|
||||
try:
|
||||
with open(src, 'rb') as f:
|
||||
self.client.storbinary('STOR '+target, f)
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, e
|
||||
|
||||
def download(self, src, target):
|
||||
self.confirm_connected()
|
||||
try:
|
||||
os.makedirs(os.path.dirname(target), 0o755, exist_ok=True)
|
||||
with open(target, 'wb') as f:
|
||||
self.client.retrbinary('RETR ' + src, f.write)
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, e
|
||||
|
||||
def delete(self, path):
|
||||
self.confirm_connected()
|
||||
if not self.exists(path):
|
||||
raise FileNotFoundError('File not exist error(%s)' % path)
|
||||
try:
|
||||
self.client.delete(path)
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, e
|
||||
|
||||
def check_dir_exist(self, d):
|
||||
pwd = self.client.pwd()
|
||||
try:
|
||||
self.client.cwd(d)
|
||||
self.client.cwd(pwd)
|
||||
return True
|
||||
except error_perm:
|
||||
return False
|
||||
|
||||
def mkdir(self, dirs):
|
||||
self.confirm_connected()
|
||||
# 创建多级目录,ftplib不支持一次创建多级目录
|
||||
dir_list = dirs.split('/')
|
||||
pwd = self.client.pwd()
|
||||
try:
|
||||
for d in dir_list:
|
||||
if not d or d in ['.']:
|
||||
continue
|
||||
# 尝试切换目录
|
||||
try:
|
||||
self.client.cwd(d)
|
||||
continue
|
||||
except:
|
||||
pass
|
||||
# 切换失败创建这个目录,再切换
|
||||
try:
|
||||
self.client.mkd(d)
|
||||
self.client.cwd(d)
|
||||
except:
|
||||
return False
|
||||
return True
|
||||
finally:
|
||||
self.client.cwd(pwd)
|
||||
|
||||
def exists(self, target):
|
||||
self.confirm_connected()
|
||||
try:
|
||||
self.client.size(target)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
self.client.close()
|
|
@ -0,0 +1,50 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import os
|
||||
from .base import ObjectStorage, LogStorage
|
||||
|
||||
|
||||
class JMSReplayStorage(ObjectStorage):
|
||||
def __init__(self, config):
|
||||
self.client = config.get("SERVICE")
|
||||
|
||||
def upload(self, src, target):
|
||||
session_id = os.path.basename(target).split('.')[0]
|
||||
ok = self.client.push_session_replay(src, session_id)
|
||||
return ok, None
|
||||
|
||||
def delete(self, path):
|
||||
return False, Exception("Not support not")
|
||||
|
||||
def exists(self, path):
|
||||
return False
|
||||
|
||||
def download(self, src, target):
|
||||
return False, Exception("Not support not")
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return 'jms'
|
||||
|
||||
|
||||
class JMSCommandStorage(LogStorage):
|
||||
def __init__(self, config):
|
||||
self.client = config.get("SERVICE")
|
||||
if not self.client:
|
||||
raise Exception("Not found app service")
|
||||
|
||||
def save(self, command):
|
||||
return self.client.push_session_command([command])
|
||||
|
||||
def bulk_save(self, command_set, raise_on_error=True):
|
||||
return self.client.push_session_command(command_set)
|
||||
|
||||
def filter(self, date_from=None, date_to=None,
|
||||
user=None, asset=None, account=None,
|
||||
input=None, session=None):
|
||||
pass
|
||||
|
||||
def count(self, date_from=None, date_to=None,
|
||||
user=None, asset=None, account=None,
|
||||
input=None, session=None):
|
||||
pass
|
|
@ -0,0 +1,77 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from .base import ObjectStorage, LogStorage
|
||||
|
||||
|
||||
class MultiObjectStorage(ObjectStorage):
|
||||
|
||||
def __init__(self, configs):
|
||||
self.configs = configs
|
||||
self.storage_list = []
|
||||
self.init_storage_list()
|
||||
|
||||
def init_storage_list(self):
|
||||
from . import get_object_storage
|
||||
if isinstance(self.configs, dict):
|
||||
configs = self.configs.values()
|
||||
else:
|
||||
configs = self.configs
|
||||
|
||||
for config in configs:
|
||||
try:
|
||||
storage = get_object_storage(config)
|
||||
self.storage_list.append(storage)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def upload(self, src, target):
|
||||
success = []
|
||||
msg = []
|
||||
|
||||
for storage in self.storage_list:
|
||||
ok, err = storage.upload(src, target)
|
||||
success.append(ok)
|
||||
msg.append(err)
|
||||
|
||||
return success, msg
|
||||
|
||||
def download(self, src, target):
|
||||
success = False
|
||||
msg = None
|
||||
|
||||
for storage in self.storage_list:
|
||||
try:
|
||||
if not storage.exists(src):
|
||||
continue
|
||||
ok, msg = storage.download(src, target)
|
||||
if ok:
|
||||
success = True
|
||||
msg = ''
|
||||
break
|
||||
except:
|
||||
pass
|
||||
return success, msg
|
||||
|
||||
def delete(self, path):
|
||||
success = True
|
||||
msg = None
|
||||
|
||||
for storage in self.storage_list:
|
||||
try:
|
||||
if storage.exists(path):
|
||||
ok, msg = storage.delete(path)
|
||||
if not ok:
|
||||
success = False
|
||||
except:
|
||||
pass
|
||||
return success, msg
|
||||
|
||||
def exists(self, path):
|
||||
for storage in self.storage_list:
|
||||
try:
|
||||
if storage.exists(path):
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
return False
|
|
@ -0,0 +1,70 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import os
|
||||
|
||||
from obs.client import ObsClient
|
||||
from .base import ObjectStorage
|
||||
|
||||
|
||||
class OBSStorage(ObjectStorage):
|
||||
def __init__(self, config):
|
||||
self.endpoint = config.get("ENDPOINT", None)
|
||||
self.bucket = config.get("BUCKET", None)
|
||||
self.access_key = config.get("ACCESS_KEY", None)
|
||||
self.secret_key = config.get("SECRET_KEY", None)
|
||||
if self.access_key and self.secret_key and self.endpoint:
|
||||
proxy_host = os.getenv("proxy_host")
|
||||
proxy_port = os.getenv("proxy_port")
|
||||
proxy_username = os.getenv("proxy_username")
|
||||
proxy_password = os.getenv("proxy_password")
|
||||
self.obsClient = ObsClient(access_key_id=self.access_key, secret_access_key=self.secret_key, server=self.endpoint, proxy_host=proxy_host, proxy_port=proxy_port, proxy_username=proxy_username, proxy_password=proxy_password)
|
||||
else:
|
||||
self.obsClient = None
|
||||
|
||||
def upload(self, src, target):
|
||||
try:
|
||||
resp = self.obsClient.putFile(self.bucket, target, src)
|
||||
if resp.status < 300:
|
||||
return True, None
|
||||
else:
|
||||
return False, resp.reason
|
||||
except Exception as e:
|
||||
return False, e
|
||||
|
||||
def exists(self, path):
|
||||
resp = self.obsClient.getObjectMetadata(self.bucket, path)
|
||||
if resp.status < 300:
|
||||
return True
|
||||
return False
|
||||
|
||||
def delete(self, path):
|
||||
try:
|
||||
resp = self.obsClient.deleteObject(self.bucket, path)
|
||||
if resp.status < 300:
|
||||
return True, None
|
||||
else:
|
||||
return False, resp.reason
|
||||
except Exception as e:
|
||||
return False, e
|
||||
|
||||
def download(self, src, target):
|
||||
try:
|
||||
os.makedirs(os.path.dirname(target), 0o755, exist_ok=True)
|
||||
resp = self.obsClient.getObject(self.bucket, src, target)
|
||||
if resp.status < 300:
|
||||
return True, None
|
||||
else:
|
||||
return False, resp.reason
|
||||
except Exception as e:
|
||||
return False, e
|
||||
|
||||
def list_buckets(self):
|
||||
resp = self.obsClient.listBuckets()
|
||||
if resp.status < 300:
|
||||
return [b.name for b in resp.body.buckets]
|
||||
else:
|
||||
raise RuntimeError(resp.status, str(resp.reason))
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return 'obs'
|
|
@ -0,0 +1,72 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import os
|
||||
import time
|
||||
|
||||
import oss2
|
||||
|
||||
from .base import ObjectStorage
|
||||
|
||||
|
||||
class OSSStorage(ObjectStorage):
|
||||
def __init__(self, config):
|
||||
self.endpoint = config.get("ENDPOINT", None)
|
||||
self.bucket = config.get("BUCKET", None)
|
||||
self.access_key = config.get("ACCESS_KEY", None)
|
||||
self.secret_key = config.get("SECRET_KEY", None)
|
||||
if self.access_key and self.secret_key:
|
||||
self.auth = oss2.Auth(self.access_key, self.secret_key)
|
||||
else:
|
||||
self.auth = None
|
||||
if self.auth and self.endpoint and self.bucket:
|
||||
self.client = oss2.Bucket(self.auth, self.endpoint, self.bucket)
|
||||
else:
|
||||
self.client = None
|
||||
|
||||
def upload(self, src, target):
|
||||
try:
|
||||
self.client.put_object_from_file(target, src)
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, e
|
||||
|
||||
def exists(self, path):
|
||||
try:
|
||||
return self.client.object_exists(path)
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
def delete(self, path):
|
||||
try:
|
||||
self.client.delete_object(path)
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, e
|
||||
|
||||
def restore(self, path):
|
||||
meta = self.client.head_object(path)
|
||||
if meta.resp.headers['x-oss-storage-class'] == oss2.BUCKET_STORAGE_CLASS_ARCHIVE:
|
||||
self.client.restore_object(path)
|
||||
while True:
|
||||
meta = self.client.head_object(path)
|
||||
if meta.resp.headers['x-oss-restore'] == 'ongoing-request="true"':
|
||||
time.sleep(5)
|
||||
else:
|
||||
break
|
||||
|
||||
def download(self, src, target):
|
||||
try:
|
||||
os.makedirs(os.path.dirname(target), 0o755, exist_ok=True)
|
||||
self.restore(src)
|
||||
self.client.get_object_to_file(src, target)
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, e
|
||||
|
||||
def list_buckets(self):
|
||||
service = oss2.Service(self.auth,self.endpoint)
|
||||
return ([b.name for b in oss2.BucketIterator(service)])
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return 'oss'
|
|
@ -0,0 +1,74 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import boto3
|
||||
import os
|
||||
|
||||
from .base import ObjectStorage
|
||||
|
||||
|
||||
class S3Storage(ObjectStorage):
|
||||
def __init__(self, config):
|
||||
self.bucket = config.get("BUCKET", "jumpserver")
|
||||
self.region = config.get("REGION", None)
|
||||
self.access_key = config.get("ACCESS_KEY", None)
|
||||
self.secret_key = config.get("SECRET_KEY", None)
|
||||
self.endpoint = config.get("ENDPOINT", None)
|
||||
|
||||
try:
|
||||
self.client = boto3.client(
|
||||
's3', region_name=self.region,
|
||||
aws_access_key_id=self.access_key,
|
||||
aws_secret_access_key=self.secret_key,
|
||||
endpoint_url=self.endpoint
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def upload(self, src, target):
|
||||
try:
|
||||
self.client.upload_file(Filename=src, Bucket=self.bucket, Key=target)
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, e
|
||||
|
||||
def exists(self, path):
|
||||
try:
|
||||
self.client.head_object(Bucket=self.bucket, Key=path)
|
||||
return True
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
def download(self, src, target):
|
||||
try:
|
||||
os.makedirs(os.path.dirname(target), 0o755, exist_ok=True)
|
||||
self.client.download_file(self.bucket, src, target)
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, e
|
||||
|
||||
def delete(self, path):
|
||||
try:
|
||||
self.client.delete_object(Bucket=self.bucket, Key=path)
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, e
|
||||
|
||||
def generate_presigned_url(self, path, expire=3600):
|
||||
try:
|
||||
return self.client.generate_presigned_url(
|
||||
ClientMethod='get_object',
|
||||
Params={'Bucket': self.bucket, 'Key': path},
|
||||
ExpiresIn=expire,
|
||||
HttpMethod='GET'), None
|
||||
except Exception as e:
|
||||
return False, e
|
||||
|
||||
def list_buckets(self):
|
||||
response = self.client.list_buckets()
|
||||
buckets = response.get('Buckets', [])
|
||||
result = [b['Name'] for b in buckets if b.get('Name')]
|
||||
return result
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return 's3'
|
|
@ -0,0 +1,109 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import io
|
||||
import os
|
||||
|
||||
import paramiko
|
||||
|
||||
from .base import ObjectStorage
|
||||
|
||||
|
||||
class SFTPStorage(ObjectStorage):
|
||||
|
||||
def __init__(self, config):
|
||||
self.sftp = None
|
||||
self.sftp_host = config.get('SFTP_HOST', None)
|
||||
self.sftp_port = int(config.get('SFTP_PORT', 22))
|
||||
self.sftp_username = config.get('SFTP_USERNAME', '')
|
||||
self.sftp_secret_type = config.get('STP_SECRET_TYPE', 'password')
|
||||
self.sftp_password = config.get('SFTP_PASSWORD', '')
|
||||
self.sftp_private_key = config.get('STP_PRIVATE_KEY', '')
|
||||
self.sftp_passphrase = config.get('STP_PASSPHRASE', '')
|
||||
self.sftp_root_path = config.get('SFTP_ROOT_PATH', '/tmp')
|
||||
self.ssh = paramiko.SSHClient()
|
||||
self.connect()
|
||||
|
||||
def connect(self):
|
||||
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
if self.sftp_secret_type == 'password':
|
||||
self.ssh.connect(self.sftp_host, self.sftp_port, self.sftp_username, self.sftp_password)
|
||||
elif self.sftp_secret_type == 'ssh_key':
|
||||
pkey = paramiko.RSAKey.from_private_key(io.StringIO(self.sftp_private_key))
|
||||
self.ssh.connect(self.sftp_host, self.sftp_port, self.sftp_username, pkey=pkey,
|
||||
passphrase=self.sftp_passphrase)
|
||||
self.sftp = self.ssh.open_sftp()
|
||||
|
||||
def confirm_connected(self):
|
||||
try:
|
||||
self.sftp.getcwd()
|
||||
except Exception as e:
|
||||
self.connect()
|
||||
|
||||
def upload(self, src, target):
|
||||
local_file = src
|
||||
remote_file = os.path.join(self.sftp_root_path, target)
|
||||
try:
|
||||
self.confirm_connected()
|
||||
mode = os.stat(local_file).st_mode
|
||||
remote_dir = os.path.dirname(remote_file)
|
||||
if not self.exists(remote_dir):
|
||||
self.sftp.mkdir(remote_dir)
|
||||
self.sftp.put(local_file, remote_file)
|
||||
self.sftp.chmod(remote_file, mode)
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, e
|
||||
|
||||
def download(self, src, target):
|
||||
remote_file = src
|
||||
local_file = target
|
||||
self.confirm_connected()
|
||||
try:
|
||||
local_dir = os.path.dirname(local_file)
|
||||
if not os.path.exists(local_dir):
|
||||
os.makedirs(local_dir)
|
||||
mode = self.sftp.stat(remote_file).st_mode
|
||||
self.sftp.get(remote_file, local_file)
|
||||
os.chmod(local_file, mode)
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, e
|
||||
|
||||
def delete(self, path):
|
||||
path = os.path.join(self.sftp_root_path, path)
|
||||
self.confirm_connected()
|
||||
if not self.exists(path):
|
||||
raise FileNotFoundError('File not exist error(%s)' % path)
|
||||
try:
|
||||
self.sftp.remove(path)
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, e
|
||||
|
||||
def check_dir_exist(self, d):
|
||||
self.confirm_connected()
|
||||
try:
|
||||
self.sftp.stat(d)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def mkdir(self, dirs):
|
||||
self.confirm_connected()
|
||||
try:
|
||||
if not self.exists(dirs):
|
||||
self.sftp.mkdir(dirs)
|
||||
return True, None
|
||||
except Exception as e:
|
||||
return False, e
|
||||
|
||||
def exists(self, target):
|
||||
self.confirm_connected()
|
||||
try:
|
||||
self.sftp.stat(target)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
self.sftp.close()
|
||||
self.ssh.close()
|
|
@ -1,11 +1,11 @@
|
|||
import os
|
||||
|
||||
import jms_storage
|
||||
from celery import shared_task
|
||||
from django.conf import settings
|
||||
from django.core.mail import send_mail, EmailMultiAlternatives, get_connection
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.storage import jms_storage
|
||||
from .utils import get_logger
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
|
|
@ -47,7 +47,7 @@ class Subscription:
|
|||
self.ch = pb.ch
|
||||
self.sub = sub
|
||||
self.unsubscribed = False
|
||||
logger.info("Subscribed to channel: ", sub)
|
||||
logger.info(f"Subscribed to channel: {sub}")
|
||||
|
||||
def _handle_msg(self, _next, error, complete):
|
||||
"""
|
||||
|
@ -106,11 +106,11 @@ class Subscription:
|
|||
|
||||
def unsubscribe(self):
|
||||
self.unsubscribed = True
|
||||
logger.info("Unsubscribed from channel: ", self.sub)
|
||||
logger.info(f"Unsubscribed from channel: {self.sub}")
|
||||
try:
|
||||
self.sub.close()
|
||||
except Exception as e:
|
||||
logger.warning('Unsubscribe msg error: {}'.format(e))
|
||||
logger.warning(f'Unsubscribe msg error: {e}')
|
||||
|
||||
def retry(self, _next, error, complete):
|
||||
logger.info('Retry subscribe channel: {}'.format(self.ch))
|
||||
|
|
|
@ -20,16 +20,20 @@ logger = get_logger(__file__)
|
|||
be executed to send SMS messages"""
|
||||
)
|
||||
)
|
||||
def send_sms_async(target, code):
|
||||
SMS().send_verify_code(target, code)
|
||||
def send_sms_async(target, code, user_info):
|
||||
SMS().send_verify_code(target, code, user_info=user_info)
|
||||
|
||||
|
||||
class SendAndVerifyCodeUtil(object):
|
||||
KEY_TMPL = 'auth-verify-code-{}'
|
||||
|
||||
def __init__(self, target, code=None, key=None, backend='email', timeout=None, **kwargs):
|
||||
def __init__(
|
||||
self, target, code=None, key=None, backend='email',
|
||||
user_info=None, timeout=None, **kwargs
|
||||
):
|
||||
self.code = code
|
||||
self.target = target
|
||||
self.user_info = user_info
|
||||
self.backend = backend
|
||||
self.key = key or self.KEY_TMPL.format(target)
|
||||
self.timeout = settings.VERIFY_CODE_TTL if timeout is None else timeout
|
||||
|
@ -78,7 +82,7 @@ class SendAndVerifyCodeUtil(object):
|
|||
return code
|
||||
|
||||
def __send_with_sms(self):
|
||||
send_sms_async.apply_async(args=(self.target, self.code), priority=100)
|
||||
send_sms_async.apply_async(args=(self.target, self.code, self.user_info), priority=100)
|
||||
|
||||
def __send_with_email(self):
|
||||
subject = self.other_args.get('subject', '')
|
||||
|
|
|
@ -11,7 +11,7 @@ class BaseTranslateManager:
|
|||
SEPARATOR = "<SEP>"
|
||||
LANG_MAPPER = {
|
||||
'ja': 'Japanese',
|
||||
'zh_hant': 'Taiwan',
|
||||
'zh_hant': 'Traditional Chinese',
|
||||
# 'en': 'English',
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -69,5 +69,7 @@
|
|||
"VerifyCode": "Verify Code",
|
||||
"WaitFileTransfer": "Wait file transfer to finish",
|
||||
"WebSocketClosed": "WebSocket closed",
|
||||
"Writable": "Writable"
|
||||
"Writable": "Writable",
|
||||
"UploadStart": "Upload start",
|
||||
"UploadEnd": "Upload completed, please wait for further processing"
|
||||
}
|
|
@ -69,5 +69,7 @@
|
|||
"VerifyCode": "認証コード",
|
||||
"WaitFileTransfer": "ファイル転送終了待ち",
|
||||
"WebSocketClosed": "WebSocket 閉店",
|
||||
"Writable": "書き込み可能"
|
||||
"Writable": "書き込み可能",
|
||||
"UploadStart": "アップロード開始",
|
||||
"UploadEnd": "アップロードが完了しました。後の処理をお待ちください"
|
||||
}
|
|
@ -65,6 +65,8 @@
|
|||
"UploadSuccess": "上传成功",
|
||||
"UploadTips": "将文件拖到此处,或点击上传",
|
||||
"UploadTitle": "上传文件",
|
||||
"UploadStart": "上传开始",
|
||||
"UploadEnd": "上传已完成,请等待后续处理",
|
||||
"User": "用户",
|
||||
"VerifyCode": "验证码",
|
||||
"WaitFileTransfer": "等待文件传输结束",
|
||||
|
|
|
@ -69,5 +69,7 @@
|
|||
"VerifyCode": "驗證碼",
|
||||
"WaitFileTransfer": "等待文件傳輸結束",
|
||||
"WebSocketClosed": "WebSocket 已關閉",
|
||||
"Writable": "讀寫"
|
||||
"Writable": "讀寫",
|
||||
"UploadStart": "上傳開始",
|
||||
"UploadEnd": "上傳已完成,請等待後續處理"
|
||||
}
|
|
@ -66,11 +66,12 @@
|
|||
"AddSuccessMsg": "Add successful",
|
||||
"AddUserGroupToThisPermission": "Add user groups",
|
||||
"AddUserToThisPermission": "Add users",
|
||||
"AddVariable": "Add Variable",
|
||||
"Address": "Address",
|
||||
"AdhocCreate": "Create the command",
|
||||
"AdhocDetail": "Command details",
|
||||
"AdhocManage": "Script",
|
||||
"AdhocUpdate": "Update the command",
|
||||
"AdhocUpdate": "Update Script",
|
||||
"Advanced": "Advanced settings",
|
||||
"AfterChange": "After changes",
|
||||
"AjaxError404": "404 request error",
|
||||
|
@ -134,7 +135,7 @@
|
|||
"AssetBulkUpdateTips": "Network devices, cloud services, web, batch updating of zones not supported",
|
||||
"AssetChangeSecretCreate": "Create account secret change",
|
||||
"AssetChangeSecretUpdate": "Update account secret change",
|
||||
"AssetData": "Asset",
|
||||
"AssetData": "Asset data",
|
||||
"AssetDetail": "Asset details",
|
||||
"AssetList": "Assets",
|
||||
"AssetListHelpMessage": "On the left is the asset tree. right-click to create, delete or modify tree nodes. assets are also organized in node form. on the right are the assets under this node. \n",
|
||||
|
@ -176,6 +177,8 @@
|
|||
"AwaitingMyApproval": "Assigned",
|
||||
"Azure": "Azure (China)",
|
||||
"Azure_Int": "Azure (International)",
|
||||
"AzureKeyVault": "Azure vault",
|
||||
"HashicorpVault": "HCP vault",
|
||||
"Backup": "Backup",
|
||||
"BackupAccountsHelpText": "Backup account information externally. it can be stored in an external system or sent via email, supporting segmented delivery.",
|
||||
"BadConflictErrorMsg": "Refreshing, please try again later",
|
||||
|
@ -199,7 +202,7 @@
|
|||
"BaseCommandFilterAclList": "Command filter",
|
||||
"BaseConnectMethodACL": "Connect Method ACL",
|
||||
"BaseFlowSetUp": "Flow Set Up",
|
||||
"BaseJobManagement": "Job Management",
|
||||
"BaseJobManagement": "Job List",
|
||||
"BaseLoginLog": "Login Log",
|
||||
"BaseMyAssets": "My Assets",
|
||||
"BaseOperateLog": "Operate Log",
|
||||
|
@ -255,6 +258,7 @@
|
|||
"ChangeField": "Change field",
|
||||
"ChangeOrganization": "Change organization",
|
||||
"ChangePassword": "Change password",
|
||||
"ChangeSecretAccountHelpText": "For accounts in the same asset, if there is a switch-from relationship, the password change should not be performed in the same task, but should be divided into two tasks for execution separately.",
|
||||
"ChangeSecretParams": "Change secret parameters",
|
||||
"ChangeViewHelpText": "Click to switch different views",
|
||||
"Chat": "Chat",
|
||||
|
@ -414,6 +418,7 @@
|
|||
"DeclassificationLogNum": "Password change logs",
|
||||
"DefaultDatabase": "Default database",
|
||||
"DefaultPort": "Default port",
|
||||
"DefaultValue": "Default value",
|
||||
"Delete": "Delete",
|
||||
"DeleteConfirmMessage": "Deletion is irreversible, do you wish to continue?",
|
||||
"DeleteErrorMsg": "Delete failed",
|
||||
|
@ -503,6 +508,7 @@
|
|||
"ExportOnlyFiltered": "Export filtered items",
|
||||
"ExportOnlySelectedItems": "Export selected items",
|
||||
"ExportRange": "Export range",
|
||||
"ExtraArgsPlaceholder": "One option per line, for example:\nOption 1: Value 1\nOption 2: Value 2",
|
||||
"FC": "Fusion compute",
|
||||
"FTPFileNotStored": "The file has not been saved to storage yet, please check back later.",
|
||||
"FTPStorageNotEnabled": "The file storage function is not enabled. Please modify the configuration file and add the following configuration: FTP_FILE_MAX_STORE=100 (supports saving files within 100M)",
|
||||
|
@ -647,8 +653,8 @@
|
|||
"JobCenter": "Job center",
|
||||
"JobCreate": "Create job",
|
||||
"JobDetail": "Job details",
|
||||
"JobExecutionLog": "Job logs",
|
||||
"JobManagement": "Jobs",
|
||||
"JobExecutionLog": "Executions",
|
||||
"JobManagement": "Job List",
|
||||
"JobUpdate": "Update the job",
|
||||
"KingSoftCloud": "KingSoft cloud",
|
||||
"KokoSetting": "KoKo",
|
||||
|
@ -674,6 +680,8 @@
|
|||
"LicenseForTest": "Test purpose license, this license is only for testing (poc) and demonstration",
|
||||
"LicenseReachedAssetAmountLimit": "The assets has exceeded the license limit",
|
||||
"LicenseWillBe": "License expiring soon",
|
||||
"ListPreference": "List preferences",
|
||||
"LoadTemplate": "Load template",
|
||||
"Loading": "Loading",
|
||||
"LockedIP": "Locked ip {count}",
|
||||
"Log": "Log",
|
||||
|
@ -819,6 +827,7 @@
|
|||
"OperateLog": "Operate logs",
|
||||
"OperationLogNum": "Operation logs",
|
||||
"Options": "Options",
|
||||
"OracleDBNameHelpText": "Fill in the SID or service name of the Oracle database (Service Name)",
|
||||
"OrgAdmin": "Organization admin",
|
||||
"OrgAuditor": "Organizational auditors",
|
||||
"OrgName": "Authorized organization",
|
||||
|
@ -899,7 +908,7 @@
|
|||
"ProfileSetting": "Profile info",
|
||||
"Project": "Project name",
|
||||
"Prompt": "Prompt",
|
||||
"Proportion": "New this week",
|
||||
"Proportion": "Proportion",
|
||||
"ProportionOfAssetTypes": "Asset type proportion",
|
||||
"Protocol": "Protocol",
|
||||
"Protocols": "Protocols",
|
||||
|
@ -1062,7 +1071,7 @@
|
|||
"Secure": "Security",
|
||||
"Security": "Security",
|
||||
"Select": "Select",
|
||||
"SelectAdhoc": "Select command",
|
||||
"SelectAdhoc": "Select command template",
|
||||
"SelectAll": "Select all",
|
||||
"SelectAtLeastOneAssetOrNodeErrMsg": "Select at least one asset or node",
|
||||
"SelectAttrs": "Select attributes",
|
||||
|
@ -1203,7 +1212,6 @@
|
|||
"SystemTasks": "Tasks",
|
||||
"SystemTools": "Tools",
|
||||
"TableColSetting": "Select visible attribute columns",
|
||||
"TableSetting": "Table preferences",
|
||||
"TagCreate": "Create tag",
|
||||
"TagInputFormatValidation": "Tag format error, the correct format is: name:value",
|
||||
"TagList": "Tags",
|
||||
|
@ -1226,6 +1234,7 @@
|
|||
"TemplateCreate": "Create template",
|
||||
"TemplateHelpText": "When selecting a template to add, accounts that do not exist under the asset will be automatically created and pushed",
|
||||
"TemplateManagement": "Templates",
|
||||
"Templates": "Templates",
|
||||
"TencentCloud": "Tencent cloud",
|
||||
"Terminal": "Components",
|
||||
"TerminalDetail": "Terminal details",
|
||||
|
@ -1354,6 +1363,7 @@
|
|||
"Valid": "Valid",
|
||||
"Variable": "Variable",
|
||||
"VariableHelpText": "You can use {{ key }} to read built-in variables in commands",
|
||||
"VariableName": "Variable name",
|
||||
"VaultHCPMountPoint": "The mount point of the Vault server, default is jumpserver",
|
||||
"VaultHelpText": "1. for security reasons, vault storage must be enabled in the configuration file.<br>2. after enabled, fill in other configurations, and perform tests.<br>3. carry out data synchronization, which is one-way, only syncing from the local database to the distant vault, once synchronization is completed, the local database will no longer store passwords, please back up your data.<br>4. after modifying vault configuration the second time, you need to restart the service.",
|
||||
"VerificationCodeSent": "Verification code has been sent",
|
||||
|
@ -1401,5 +1411,12 @@
|
|||
"ZoneUpdate": "Update the zone",
|
||||
"disallowSelfUpdateFields": "Not allowed to modify the current fields yourself",
|
||||
"forceEnableMFAHelpText": "If force enable, user can not disable by themselves",
|
||||
"removeWarningMsg": "Are you sure you want to remove"
|
||||
"removeWarningMsg": "Are you sure you want to remove",
|
||||
"setVariable": "Set variable",
|
||||
"JobsAudit": "Job audits",
|
||||
"JobList": "Job List",
|
||||
"StopJobMsg": "Stop job successfully",
|
||||
"ExtraArgsFormatError": "Format error, please enter according to the requirements",
|
||||
"MFAOnlyAdminUsers": "Globally: Only admin",
|
||||
"MFAAllUsers": "Globally: All users"
|
||||
}
|
|
@ -66,11 +66,12 @@
|
|||
"AddSuccessMsg": "追加成功",
|
||||
"AddUserGroupToThisPermission": "ユーザーグループを追加",
|
||||
"AddUserToThisPermission": "ユーザーを追加する",
|
||||
"AddVariable": "パラメータを追加",
|
||||
"Address": "アドレス",
|
||||
"AdhocCreate": "アドホックコマンドを作成",
|
||||
"AdhocDetail": "コマンド詳細",
|
||||
"AdhocManage": "スクリプト管理",
|
||||
"AdhocUpdate": "コマンドを更新",
|
||||
"AdhocUpdate": "更新スクリプト",
|
||||
"Advanced": "高度な設定",
|
||||
"AfterChange": "変更後",
|
||||
"AjaxError404": "404 リクエストエラー",
|
||||
|
@ -176,6 +177,8 @@
|
|||
"AwaitingMyApproval": "私の承認待ち",
|
||||
"Azure": "Azure(中国)",
|
||||
"Azure_Int": "アジュール(インターナショナル)",
|
||||
"AzureKeyVault": "Azure vault",
|
||||
"HashicorpVault": "HCP vault",
|
||||
"Backup": "バックアップ",
|
||||
"BackupAccountsHelpText": "アカウント情報を外部にバックアップする。外部システムに保存するかメールを送信することもできます、セクション方式をサポートしています",
|
||||
"BadConflictErrorMsg": "更新中です、しばらくお待ちください",
|
||||
|
@ -199,7 +202,7 @@
|
|||
"BaseCommandFilterAclList": "コマンドフィルタ",
|
||||
"BaseConnectMethodACL": "接続方法の承認",
|
||||
"BaseFlowSetUp": "フロー設定",
|
||||
"BaseJobManagement": "作業管理",
|
||||
"BaseJobManagement": "作業列表",
|
||||
"BaseLoginLog": "ログインログ",
|
||||
"BaseMyAssets": "私の資産",
|
||||
"BaseOperateLog": "Actionログ",
|
||||
|
@ -269,6 +272,7 @@
|
|||
"ChangeOrganization": "組織の 변경",
|
||||
"ChangePassword": "パスワード更新",
|
||||
"ChangeReceiver": "メッセージ受信者の変更",
|
||||
"ChangeSecretAccountHelpText": "同じ資産内のアカウントに切り替え元関係がある場合、パスワード変更は同じタスクで実行せず、2 つのタスクに分割して別々に実行する必要があります。",
|
||||
"ChangeSecretParams": "パスワード変更パラメータ",
|
||||
"ChangeViewHelpText": "クリックして異なるビューを切り替え",
|
||||
"Chat": "チャット",
|
||||
|
@ -429,6 +433,7 @@
|
|||
"DeclassificationLogNum": "パスワード変更ログ数",
|
||||
"DefaultDatabase": "デフォルトのデータベース",
|
||||
"DefaultPort": "デフォルトポート",
|
||||
"DefaultValue": "デフォルト値",
|
||||
"Delete": "削除",
|
||||
"DeleteConfirmMessage": "一度削除すると復元はできません、続けますか?",
|
||||
"DeleteErrorMsg": "削除に失敗",
|
||||
|
@ -506,9 +511,10 @@
|
|||
"ExcludeSymbol": "文字の除外",
|
||||
"ExecCloudSyncErrorMsg": "クラウドアカウントの設定が完全でないので、更新して再試行してください",
|
||||
"Execute": "実行",
|
||||
"ExecuteAfterSaving": "保存後に実行",
|
||||
"ExecuteOnce": "一度実行する",
|
||||
"ExecutionDetail": "Action詳細",
|
||||
"ExecutionList": "実行リスト",
|
||||
"ExecutionList": "実行記録",
|
||||
"ExistError": "この要素は既に存在します",
|
||||
"Existing": "既に存在しています",
|
||||
"ExpirationTimeout": "有効期限タイムアウト(秒)",
|
||||
|
@ -519,6 +525,8 @@
|
|||
"ExportOnlyFiltered": "検索結果のみをエクスポート",
|
||||
"ExportOnlySelectedItems": "選択オプションのみをエクスポート",
|
||||
"ExportRange": "エクスポート範囲",
|
||||
"ExtraArgsFormatError": "書式が間違っています。要件に従って入力してください",
|
||||
"ExtraArgsPlaceholder": "一行ごとに一つの選択肢を書く、例えば:\n選択肢1:値1\n選択肢2:値2\n",
|
||||
"FC": "Fusion Compute",
|
||||
"FTPFileNotStored": "ファイルはまだストレージに保存されていません、後で確認してください。",
|
||||
"FTPStorageNotEnabled": "ファイルストレージ機能が有効になっていません、設定ファイルを変更し、次の設定を追加してください:FTP_FILE_MAX_STORE=100(100M以下のファイルを保存可能)",
|
||||
|
@ -668,9 +676,11 @@
|
|||
"JobCenter": "Actionセンター",
|
||||
"JobCreate": "ジョブ作成",
|
||||
"JobDetail": "作業詳細",
|
||||
"JobExecutionLog": "作業ログ",
|
||||
"JobManagement": "作業管理",
|
||||
"JobExecutionLog": "実行記録",
|
||||
"JobList": "作業リスト",
|
||||
"JobManagement": "作業列表",
|
||||
"JobUpdate": "アップデート作業",
|
||||
"JobsAudit": "作業の監査",
|
||||
"KingSoftCloud": "Kingsoftクラウド",
|
||||
"KokoSetting": "KoKo 設定",
|
||||
"LAN": "LAN",
|
||||
|
@ -700,6 +710,8 @@
|
|||
"LicenseForTest": "テスト用ライセンス。このライセンスはテスト(PoC)とデモンストレーションにのみ使用されます",
|
||||
"LicenseReachedAssetAmountLimit": "資産の数量がライセンスの数量制限を超えています",
|
||||
"LicenseWillBe": "ライセンスは間もなく ",
|
||||
"ListPreference": "リスト設定",
|
||||
"LoadTemplate": "テンプレートからロード",
|
||||
"Loading": "読み込み中",
|
||||
"LockedIP": "IP {count} つがロックされました",
|
||||
"Log": "ログ",
|
||||
|
@ -851,6 +863,7 @@
|
|||
"OperateLog": "操作ログ",
|
||||
"OperationLogNum": "Actionログ数",
|
||||
"Options": "オプション",
|
||||
"OracleDBNameHelpText": "Oracle データベースの SID またはサービス名 (サービス名) を入力します。",
|
||||
"OrgAdmin": "管理",
|
||||
"OrgAuditor": "組織監査員",
|
||||
"OrgName": "Actionグループの名前",
|
||||
|
@ -1053,7 +1066,7 @@
|
|||
"Rules": "規則",
|
||||
"Run": "Action",
|
||||
"RunAgain": "再実行",
|
||||
"RunAs": "実行ユーザー",
|
||||
"RunAs": "実行アカウント (じっこうアカウント)",
|
||||
"RunCommand": "コマンドの実行",
|
||||
"RunJob": "ジョブを実行",
|
||||
"RunSucceed": "タスクが成功",
|
||||
|
@ -1097,7 +1110,7 @@
|
|||
"Secure": "安全",
|
||||
"Security": "セキュリティ設定",
|
||||
"Select": "選択",
|
||||
"SelectAdhoc": "コマンドの選択",
|
||||
"SelectAdhoc": "スクリプトテンプレートを選択",
|
||||
"SelectAll": "全選択",
|
||||
"SelectAtLeastOneAssetOrNodeErrMsg": "アセットまたはノードは少なくとも一つ選択してください",
|
||||
"SelectAttrs": "属性の選択",
|
||||
|
@ -1182,6 +1195,7 @@
|
|||
"StatusYellow": "最近、実行に失敗があり。",
|
||||
"Step": "ステップ",
|
||||
"Stop": "停止",
|
||||
"StopJobMsg": "成功を停止",
|
||||
"StopLogOutput": "ask Canceled:現在のタスク(currentTaskId)は手動で停止されました。各タスクの進行状況が異なるため、以下はタスクの最終実行結果です。実行が失敗した場合は、タスクが正常に停止されました。",
|
||||
"Storage": "ストレージ",
|
||||
"StorageSetting": "ストレージ設定",
|
||||
|
@ -1244,7 +1258,6 @@
|
|||
"SystemTasks": "タスクリスト",
|
||||
"SystemTools": "システムツール",
|
||||
"TableColSetting": "表示属性列の選択",
|
||||
"TableSetting": "テーブル環境設定",
|
||||
"TagCreate": "ラベルの作成",
|
||||
"TagInputFormatValidation": "ラベルの形式が間違っています、正しい形式は:name:value",
|
||||
"TagList": "タグ一覧",
|
||||
|
@ -1266,7 +1279,8 @@
|
|||
"TemplateAdd": "テンプレート追加",
|
||||
"TemplateCreate": "テンプレート作成",
|
||||
"TemplateHelpText": "テンプレートを選択して追加すると、資産の下に存在しないアカウントが自動的に作成され、プッシュされます",
|
||||
"TemplateManagement": "テンプレート管理",
|
||||
"TemplateManagement": "テンプレート一覧",
|
||||
"Templates": "テンプレート",
|
||||
"TencentCloud": "テンセントクラウド",
|
||||
"Terminal": "コンポーネント設定",
|
||||
"TerminalDetail": "コンポーネントの詳細",
|
||||
|
@ -1396,6 +1410,7 @@
|
|||
"Valid": "有効",
|
||||
"Variable": "変数",
|
||||
"VariableHelpText": "コマンド中で {{ key }} を使用して内蔵変数を読み取ることができます",
|
||||
"VariableName": "変数名",
|
||||
"VaultHCPMountPoint": "Vault サーバのマウントポイント、デフォルトはjumpserver",
|
||||
"VaultHelpText": "1. セキュリティ上の理由により、設定ファイルで Vault ストレージをオンにする必要があります。<br>2. オンにした後、他の設定を入力してテストを行います。<br>3. データ同期を行います。同期は一方向です。ローカルデータベースからリモートの Vault にのみ同期します。同期が終了すればローカルデータベースはパスワードを保管していませんので、データのバックアップをお願いします。<br>4. Vault の設定を二度変更した後はサービスを再起動する必要があります。",
|
||||
"VerificationCodeSent": "認証コードが送信されました",
|
||||
|
@ -1443,5 +1458,6 @@
|
|||
"ZoneUpdate": "更新エリア",
|
||||
"disallowSelfUpdateFields": "現在のフィールドを自分で変更することは許可されていません",
|
||||
"forceEnableMFAHelpText": "強制的に有効化すると、ユーザーは自分で無効化することができません。",
|
||||
"removeWarningMsg": "削除してもよろしいですか"
|
||||
"removeWarningMsg": "削除してもよろしいですか",
|
||||
"setVariable": "パラメータ設定"
|
||||
}
|
|
@ -66,11 +66,12 @@
|
|||
"AddSuccessMsg": "添加成功",
|
||||
"AddUserGroupToThisPermission": "添加用户组",
|
||||
"AddUserToThisPermission": "添加用户",
|
||||
"AddVariable": "添加参数",
|
||||
"Address": "地址",
|
||||
"AdhocCreate": "创建命令",
|
||||
"AdhocDetail": "命令详情",
|
||||
"AdhocManage": "脚本管理",
|
||||
"AdhocUpdate": "更新命令",
|
||||
"AdhocUpdate": "更新脚本",
|
||||
"Advanced": "高级设置",
|
||||
"AfterChange": "变更后",
|
||||
"AjaxError404": "404 请求错误",
|
||||
|
@ -176,6 +177,8 @@
|
|||
"AwaitingMyApproval": "待我审批",
|
||||
"Azure": "Azure (中国)",
|
||||
"Azure_Int": "Azure (国际)",
|
||||
"AzureKeyVault": "Azure vault",
|
||||
"HashicorpVault": "HCP vault",
|
||||
"Backup": "备份",
|
||||
"BackupAccountsHelpText": "备份账号信息到外部。可以存储到外部系统或发送邮件,支持分段方式",
|
||||
"BadConflictErrorMsg": "正在刷新中,请稍后再试",
|
||||
|
@ -199,7 +202,7 @@
|
|||
"BaseCommandFilterAclList": "命令过滤",
|
||||
"BaseConnectMethodACL": "连接方式授权",
|
||||
"BaseFlowSetUp": "流程设置",
|
||||
"BaseJobManagement": "作业管理",
|
||||
"BaseJobManagement": "作业列表",
|
||||
"BaseLoginLog": "登录日志",
|
||||
"BaseMyAssets": "我的资产",
|
||||
"BaseOperateLog": "操作日志",
|
||||
|
@ -255,6 +258,7 @@
|
|||
"ChangeField": "变更字段",
|
||||
"ChangeOrganization": "更改组织",
|
||||
"ChangePassword": "更新密码",
|
||||
"ChangeSecretAccountHelpText": "对于同一资产中的账号,如果存在切换自关系,则不应放在同一个任务中执行密码更改,而是应分成两个任务分别执行。",
|
||||
"ChangeSecretParams": "改密参数",
|
||||
"ChangeViewHelpText": "点击切换不同视图",
|
||||
"Chat": "聊天",
|
||||
|
@ -414,6 +418,7 @@
|
|||
"DeclassificationLogNum": "改密日志数",
|
||||
"DefaultDatabase": "默认数据库",
|
||||
"DefaultPort": "默认端口",
|
||||
"DefaultValue": "默认值",
|
||||
"Delete": "删除",
|
||||
"DeleteConfirmMessage": "删除后无法恢复,是否继续?",
|
||||
"DeleteErrorMsg": "删除失败",
|
||||
|
@ -491,9 +496,10 @@
|
|||
"ExcludeSymbol": "排除字符",
|
||||
"ExecCloudSyncErrorMsg": "云账号配置不完整,请更新后重试",
|
||||
"Execute": "执行",
|
||||
"ExecuteAfterSaving": "保存后执行",
|
||||
"ExecuteOnce": "执行一次",
|
||||
"ExecutionDetail": "执行详情",
|
||||
"ExecutionList": "执行列表",
|
||||
"ExecutionList": "执行记录",
|
||||
"ExistError": "这个元素已经存在",
|
||||
"Existing": "已存在",
|
||||
"ExpirationTimeout": "过期超时时间(秒)",
|
||||
|
@ -504,6 +510,7 @@
|
|||
"ExportOnlyFiltered": "仅导出搜索结果",
|
||||
"ExportOnlySelectedItems": "仅导出选择项",
|
||||
"ExportRange": "导出范围",
|
||||
"ExtraArgsPlaceholder": "每行一个选项,例如:\n选项1:值1\n选项2:值2\n",
|
||||
"FC": "Fusion Compute",
|
||||
"FTPFileNotStored": "文件尚未保存到存储中,请稍后查看。",
|
||||
"FTPStorageNotEnabled": "文件存储功能未开启,请修改配置文件并添加配置:FTP_FILE_MAX_STORE=100(支持保存100M以内的文件)",
|
||||
|
@ -650,8 +657,8 @@
|
|||
"JobCenter": "作业中心",
|
||||
"JobCreate": "创建作业",
|
||||
"JobDetail": "作业详情",
|
||||
"JobExecutionLog": "作业日志",
|
||||
"JobManagement": "作业管理",
|
||||
"JobExecutionLog": "执行记录",
|
||||
"JobManagement": "作业列表",
|
||||
"JobUpdate": "更新作业",
|
||||
"KingSoftCloud": "金山云",
|
||||
"KokoSetting": "KoKo 配置",
|
||||
|
@ -677,6 +684,8 @@
|
|||
"LicenseForTest": "测试用途许可证, 本许可证仅用于 测试(PoC)和演示",
|
||||
"LicenseReachedAssetAmountLimit": "资产数量已经超过许可证数量限制",
|
||||
"LicenseWillBe": "许可证即将在 ",
|
||||
"ListPreference": "列表偏好",
|
||||
"LoadTemplate": "从模板中加载",
|
||||
"Loading": "加载中",
|
||||
"LockedIP": "已锁定 IP {count} 个",
|
||||
"Log": "日志",
|
||||
|
@ -822,6 +831,7 @@
|
|||
"OperateLog": "操作日志",
|
||||
"OperationLogNum": "操作日志数",
|
||||
"Options": "选项",
|
||||
"OracleDBNameHelpText": "填写 Oracle 资料库的SID或服务名称(Service Name)",
|
||||
"OrgAdmin": "组织管理员",
|
||||
"OrgAuditor": "组织审计员",
|
||||
"OrgName": "授权组织名称",
|
||||
|
@ -1022,7 +1032,7 @@
|
|||
"Rules": "规则",
|
||||
"Run": "执行",
|
||||
"RunAgain": "再次执行",
|
||||
"RunAs": "运行用户",
|
||||
"RunAs": "运行账号",
|
||||
"RunCommand": "运行命令",
|
||||
"RunJob": "运行作业",
|
||||
"RunSucceed": "任务执行成功",
|
||||
|
@ -1066,7 +1076,7 @@
|
|||
"Secure": "安全",
|
||||
"Security": "安全设置",
|
||||
"Select": "选择",
|
||||
"SelectAdhoc": "选择命令",
|
||||
"SelectAdhoc": "选择脚本模板",
|
||||
"SelectAll": "全选",
|
||||
"SelectAtLeastOneAssetOrNodeErrMsg": "资产或者节点至少选择一项",
|
||||
"SelectAttrs": "选择属性",
|
||||
|
@ -1207,7 +1217,6 @@
|
|||
"SystemTasks": "任务列表",
|
||||
"SystemTools": "系统工具",
|
||||
"TableColSetting": "选择可见属性列",
|
||||
"TableSetting": "表格偏好",
|
||||
"TagCreate": "创建标签",
|
||||
"TagInputFormatValidation": "标签格式错误,正确格式为:name:value",
|
||||
"TagList": "标签列表",
|
||||
|
@ -1229,7 +1238,8 @@
|
|||
"TemplateAdd": "模版添加",
|
||||
"TemplateCreate": "创建模版",
|
||||
"TemplateHelpText": "选择模版添加时,会自动创建资产下不存在的账号并推送",
|
||||
"TemplateManagement": "模版管理",
|
||||
"TemplateManagement": "模版列表",
|
||||
"Templates": "模板",
|
||||
"TencentCloud": "腾讯云",
|
||||
"Terminal": "组件设置",
|
||||
"TerminalDetail": "组件详情",
|
||||
|
@ -1358,6 +1368,7 @@
|
|||
"Valid": "有效",
|
||||
"Variable": "变量",
|
||||
"VariableHelpText": "您可以在命令中使用 {{ key }} 读取内置变量",
|
||||
"VariableName": "变量名",
|
||||
"VaultHCPMountPoint": "Vault 服务器的挂载点,默认为 jumpserver",
|
||||
"VaultHelpText": "1. 由于安全原因,需要配置文件中开启 Vault 存储。<br>2. 开启后,填写其他配置,进行测试。<br>3. 进行数据同步,同步是单向的,只会从本地数据库同步到远端 Vault,同步完成本地数据库不再存储密码,请备份好数据。<br>4. 二次修改 Vault 配置后需重启服务。",
|
||||
"VerificationCodeSent": "验证码已发送",
|
||||
|
@ -1405,5 +1416,12 @@
|
|||
"ZoneUpdate": "更新网域",
|
||||
"disallowSelfUpdateFields": "不允许自己修改当前字段",
|
||||
"forceEnableMFAHelpText": "如果强制启用,用户无法自行禁用",
|
||||
"removeWarningMsg": "你确定要移除"
|
||||
"removeWarningMsg": "你确定要移除",
|
||||
"setVariable": "设置参数",
|
||||
"JobsAudit": "作业审计",
|
||||
"JobList": "作业列表",
|
||||
"StopJobMsg": "停止成功",
|
||||
"ExtraArgsFormatError": "格式错误,请按要求输入",
|
||||
"MFAOnlyAdminUsers": "全局启用: 仅管理员",
|
||||
"MFAAllUsers": "全局启用: 所有用户"
|
||||
}
|
|
@ -83,12 +83,13 @@
|
|||
"AddSystemUser": "添加系統用戶",
|
||||
"AddUserGroupToThisPermission": "新增使用者組",
|
||||
"AddUserToThisPermission": "新增使用者",
|
||||
"AddVariable": "添加參數",
|
||||
"Address": "地址",
|
||||
"Addressee": "收件人",
|
||||
"AdhocCreate": "創建命令",
|
||||
"AdhocDetail": "命令詳情",
|
||||
"AdhocManage": "腳本管理",
|
||||
"AdhocUpdate": "更新命令",
|
||||
"AdhocUpdate": "更新腳本",
|
||||
"Admin": "管理員",
|
||||
"AdminUser": "特權用戶",
|
||||
"AdminUserCreate": "創建管理用戶",
|
||||
|
@ -237,6 +238,8 @@
|
|||
"AwaitingMyApproval": "待我審批",
|
||||
"Azure": "Azure (中國)",
|
||||
"Azure_Int": "Azure (國際)",
|
||||
"AzureKeyVault": "Azure vault",
|
||||
"HashicorpVault": "HCP vault",
|
||||
"Backup": "備份",
|
||||
"BackupAccountsHelpText": "備份帳號資訊至外部。可以儲存到外部系統或寄送郵件,支援分段方式",
|
||||
"BadConflictErrorMsg": "正在刷新中,請稍後再試",
|
||||
|
@ -260,7 +263,7 @@
|
|||
"BaseCommandFilterAclList": "命令過濾",
|
||||
"BaseConnectMethodACL": "連接方式授權",
|
||||
"BaseFlowSetUp": "流程設定",
|
||||
"BaseJobManagement": "作業",
|
||||
"BaseJobManagement": "作業列表",
|
||||
"BaseLoginLog": "登入日誌",
|
||||
"BaseMyAssets": "我的資產",
|
||||
"BaseOperateLog": "操作日誌",
|
||||
|
@ -341,6 +344,7 @@
|
|||
"ChangeOrganization": "更改組織",
|
||||
"ChangePassword": "更改密碼",
|
||||
"ChangeReceiver": "修改消息接收人",
|
||||
"ChangeSecretAccountHelpText": "對於同一資產中的帳號,如果存在切換自關係,則不應放在同一個任務中執行密碼更改,而是應分成兩個任務分別執行。",
|
||||
"ChangeSecretParams": "改密參數",
|
||||
"ChangeViewHelpText": "點擊切換不同視圖",
|
||||
"Charset": "字元集",
|
||||
|
@ -549,6 +553,7 @@
|
|||
"DefaultDatabase": "默認資料庫",
|
||||
"DefaultPort": "默認埠",
|
||||
"DefaultProtocol": "默認協議, 添加資產時預設會選擇",
|
||||
"DefaultValue": "預設值",
|
||||
"Defaults": "預設值",
|
||||
"Delete": "刪除",
|
||||
"DeleteConfirmMessage": "刪除後無法恢復,是否繼續?",
|
||||
|
@ -648,12 +653,13 @@
|
|||
"ExcludeSymbol": "排除字元",
|
||||
"ExecCloudSyncErrorMsg": "雲帳號配置不完整,請更新後重試",
|
||||
"Execute": "執行",
|
||||
"ExecuteAfterSaving": "保存後執行",
|
||||
"ExecuteCycle": "執行週期",
|
||||
"ExecuteFailedCommand": "執行失敗命令",
|
||||
"ExecuteOnce": "執行一次",
|
||||
"Execution": "執行歷史",
|
||||
"ExecutionDetail": "執行詳情",
|
||||
"ExecutionList": "執行列表",
|
||||
"ExecutionList": "執行記錄",
|
||||
"ExecutionTimes": "執行次數",
|
||||
"ExistError": "這個元素已經存在",
|
||||
"Existing": "已存在",
|
||||
|
@ -666,6 +672,8 @@
|
|||
"ExportOnlyFiltered": "僅匯出搜索結果",
|
||||
"ExportOnlySelectedItems": "僅匯出選擇項",
|
||||
"ExportRange": "匯出範圍",
|
||||
"ExtraArgsFormatError": "格式錯誤,請按要求輸入",
|
||||
"ExtraArgsPlaceholder": "每行一個選項,例如:\n選項1:值1\n選項2:值2\n",
|
||||
"FAILURE": "失敗",
|
||||
"FC": "Fusion Compute",
|
||||
"FTPFileNotStored": "檔案尚未儲存到儲存中,請稍後查看。",
|
||||
|
@ -845,12 +853,13 @@
|
|||
"JobCenter": "作業中心",
|
||||
"JobCreate": "創建作業",
|
||||
"JobDetail": "作業詳情",
|
||||
"JobExecutionLog": "作業日誌",
|
||||
"JobExecutionLog": "執行記錄",
|
||||
"JobList": "作業管理",
|
||||
"JobManagement": "作業",
|
||||
"JobManagement": "作業列表",
|
||||
"JobName": "作業名稱",
|
||||
"JobType": "作業類型",
|
||||
"JobUpdate": "更新作業",
|
||||
"JobsAudit": "作業審核",
|
||||
"Key": "鍵",
|
||||
"KingSoftCloud": "金山雲",
|
||||
"KokoSetting": "KoKo 配置",
|
||||
|
@ -898,7 +907,9 @@
|
|||
"LicenseWillBe": "許可證即將在 ",
|
||||
"LinuxAdminUser": "Linux 特權用戶",
|
||||
"LinuxUserAffiliateGroup": "用戶附屬組",
|
||||
"ListPreference": "列表偏好",
|
||||
"LoadStatus": "負載狀態",
|
||||
"LoadTemplate": "從模版中加載",
|
||||
"Loading": "載入中",
|
||||
"LockedIP": "已鎖定 IP {count} 個",
|
||||
"Log": "日誌",
|
||||
|
@ -1089,7 +1100,7 @@
|
|||
"OperationLogNum": "操作日誌數",
|
||||
"Ops": "任務",
|
||||
"Options": "選項",
|
||||
"OracleDBNameHelpText": "提示:填寫 Oracle 資料庫的SID或服務名稱(Service Name)",
|
||||
"OracleDBNameHelpText": "填寫 Oracle 資料庫的SID或服務名稱(Service Name)",
|
||||
"OrgAdmin": "組織管理員",
|
||||
"OrgAuditor": "組織審計員",
|
||||
"OrgName": "授權組織名稱",
|
||||
|
@ -1356,7 +1367,7 @@
|
|||
"Rules": "規則",
|
||||
"Run": "運行",
|
||||
"RunAgain": "再次執行",
|
||||
"RunAs": "執行使用者",
|
||||
"RunAs": "運行賬號",
|
||||
"RunCommand": "運行命令",
|
||||
"RunJob": "運行作業",
|
||||
"RunSucceed": "任務執行成功",
|
||||
|
@ -1413,7 +1424,7 @@
|
|||
"SecuritySetting": "安全設定",
|
||||
"Select": "選擇",
|
||||
"SelectAccount": "選擇帳號",
|
||||
"SelectAdhoc": "選擇命令",
|
||||
"SelectAdhoc": "選擇腳本模板",
|
||||
"SelectAll": "全選",
|
||||
"SelectAtLeastOneAssetOrNodeErrMsg": "資產或者節點至少選擇一項",
|
||||
"SelectAttrs": "選擇屬性",
|
||||
|
@ -1507,6 +1518,7 @@
|
|||
"Step": "步驟",
|
||||
"Stop": "停止",
|
||||
"StopJob": "停止作業",
|
||||
"StopJobMsg": "停止成功",
|
||||
"StopLogOutput": "任務已取消:當前任務(currentTaskId)已被手動停止。由於每個任務的執行進度不同,以下是任務的最終執行結果。執行失敗表示任務已成功停止。",
|
||||
"Storage": "儲存設置",
|
||||
"StorageConfiguration": "儲存配置",
|
||||
|
@ -1587,7 +1599,6 @@
|
|||
"SystemUsers": "系統用戶",
|
||||
"TableColSetting": "選擇可見屬性列",
|
||||
"TableColSettingInfo": "請選擇您想顯示的列表詳細資訊。",
|
||||
"TableSetting": "表單偏好",
|
||||
"TagCreate": "創建標籤",
|
||||
"TagInputFormatValidation": "Label format error, correct format is: name:value",
|
||||
"TagList": "標籤列表",
|
||||
|
@ -1609,14 +1620,14 @@
|
|||
"TempPassword": "臨時密碼有效期為 300 秒,使用後立刻失效",
|
||||
"TempPasswordTip": "臨時密碼有效時間為 300 秒,使用後立即失效",
|
||||
"TempToken": "臨時密碼",
|
||||
"Template": "模板管理",
|
||||
"Template": "模板列表",
|
||||
"TemplateAdd": "模板添加",
|
||||
"TemplateCreate": "創建模板",
|
||||
"TemplateDetail": "模板詳情",
|
||||
"TemplateHelpText": "選擇模板添加時,會自動創建資產下不存在的帳號並推送",
|
||||
"TemplateManagement": "模板管理",
|
||||
"TemplateManagement": "模版列表",
|
||||
"TemplateUpdate": "更新模板",
|
||||
"Templates": "模板管理",
|
||||
"Templates": "模板列表",
|
||||
"TencentCloud": "騰訊雲",
|
||||
"Terminal": "組件設置",
|
||||
"TerminalDetail": "組件詳情",
|
||||
|
@ -1792,6 +1803,7 @@
|
|||
"Value": "值",
|
||||
"Variable": "變數",
|
||||
"VariableHelpText": "您可以在命令中使用 {{ key }} 讀取內建變數",
|
||||
"VariableName": "變數名",
|
||||
"Vault": "密碼匣子",
|
||||
"VaultHCPMountPoint": "重新嘗試所選",
|
||||
"VaultHelpText": "1. 由於安全原因,需要配置文件中開啟 Vault 儲存。<br>2. 開啟後,填寫其他配置,進行測試。<br>3. 進行數據同步,同步是單向的,只會從本地資料庫同步到遠端 Vault,同步完成本地資料庫不再儲存密碼,請備份好數據。<br>4. 二次修改 Vault 配置後需重啟服務。",
|
||||
|
@ -2161,7 +2173,7 @@
|
|||
"riskLevel": "風險等級",
|
||||
"rows": "行",
|
||||
"run": "執行",
|
||||
"runAs": "運行用戶",
|
||||
"runAs": "運行賬號",
|
||||
"runSucceed": "任務執行成功",
|
||||
"runTimes": "執行次數",
|
||||
"running": "運行中",
|
||||
|
@ -2192,6 +2204,7 @@
|
|||
"setFeiShu": "設置飛書認證",
|
||||
"setLark": "設置 Lark 認證",
|
||||
"setSlack": "設置 Slack 認證",
|
||||
"setVariable": "設置參數",
|
||||
"setWeCom": "設置企業微信認證",
|
||||
"setting": "設置",
|
||||
"siteUrl": "當前站點URL",
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
"Close Right Tabs": "Close Right Tabs",
|
||||
"Close split connect": "Close split connect",
|
||||
"Collapse": "Collapse",
|
||||
"Command": "Command",
|
||||
"Command Line": "Command Line",
|
||||
"Command line": "Command line",
|
||||
"CommandBar": "Command bar",
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
"Close Other Tabs": "その他を閉じる",
|
||||
"Close Right Tabs": "右側を閉じる",
|
||||
"Close split connect": "分割表示を閉じる",
|
||||
"Command": "命令",
|
||||
"Command Line": "コマンドライン",
|
||||
"Command line": "命令行",
|
||||
"CommandBar": "コマンドバー",
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
"Close Other Tabs": "关闭其它",
|
||||
"Close Right Tabs": "关闭右侧",
|
||||
"Close split connect": "关闭分屏",
|
||||
"Command": "命令",
|
||||
"Command Line": "命令行",
|
||||
"Command line": "连接命令行",
|
||||
"CommandBar": "命令栏",
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
"Close Other Tabs": "關閉其它",
|
||||
"Close Right Tabs": "關閉右側",
|
||||
"Close split connect": "關閉分屏",
|
||||
"Command": "指令",
|
||||
"Command Line": "命令行",
|
||||
"Command line": "連接命令行",
|
||||
"CommandBar": "指令欄",
|
||||
|
|
|
@ -258,10 +258,17 @@ class Config(dict):
|
|||
|
||||
# Vault
|
||||
'VAULT_ENABLED': False,
|
||||
'VAULT_BACKEND': 'local',
|
||||
|
||||
'VAULT_HCP_HOST': '',
|
||||
'VAULT_HCP_TOKEN': '',
|
||||
'VAULT_HCP_MOUNT_POINT': 'jumpserver',
|
||||
|
||||
'VAULT_AZURE_HOST': '',
|
||||
'VAULT_AZURE_CLIENT_ID': '',
|
||||
'VAULT_AZURE_CLIENT_SECRET': '',
|
||||
'VAULT_AZURE_TENANT_ID': '',
|
||||
|
||||
'HISTORY_ACCOUNT_CLEAN_LIMIT': 999,
|
||||
|
||||
# Cache login password
|
||||
|
@ -346,6 +353,7 @@ class Config(dict):
|
|||
'AUTH_OPENID_REALM_NAME': None,
|
||||
'OPENID_ORG_IDS': [DEFAULT_ID],
|
||||
|
||||
|
||||
# Raidus 认证
|
||||
'AUTH_RADIUS': False,
|
||||
'RADIUS_SERVER': 'localhost',
|
||||
|
@ -480,6 +488,12 @@ class Config(dict):
|
|||
'LOGIN_REDIRECT_TO_BACKEND': '', # 'OPENID / CAS / SAML2
|
||||
'LOGIN_REDIRECT_MSG_ENABLED': True,
|
||||
|
||||
|
||||
# 人脸识别
|
||||
'FACE_RECOGNITION_ENABLED': False,
|
||||
'FACE_RECOGNITION_DISTANCE_THRESHOLD': 0.35,
|
||||
'FACE_RECOGNITION_COSINE_THRESHOLD': 0.95,
|
||||
|
||||
'SMS_ENABLED': False,
|
||||
'SMS_BACKEND': '',
|
||||
'SMS_CODE_LENGTH': 4,
|
||||
|
|
|
@ -18,10 +18,13 @@ if not XPACK_DISABLED:
|
|||
XPACK_TEMPLATES_DIR = []
|
||||
XPACK_CONTEXT_PROCESSOR = []
|
||||
XPACK_LICENSE_IS_VALID = False
|
||||
XPACK_LICENSE_EDITION = ""
|
||||
XPACK_LICENSE_INFO = {
|
||||
'corporation': corporation,
|
||||
}
|
||||
|
||||
XPACK_LICENSE_CONTENT = 'community'
|
||||
|
||||
if XPACK_ENABLED:
|
||||
from xpack.utils import get_xpack_templates_dir, get_xpack_context_processor
|
||||
|
||||
|
|
|
@ -235,10 +235,17 @@ AUTH_TEMP_TOKEN = CONFIG.AUTH_TEMP_TOKEN
|
|||
|
||||
# Vault
|
||||
VAULT_ENABLED = CONFIG.VAULT_ENABLED
|
||||
VAULT_BACKEND = CONFIG.VAULT_BACKEND
|
||||
|
||||
VAULT_HCP_HOST = CONFIG.VAULT_HCP_HOST
|
||||
VAULT_HCP_TOKEN = CONFIG.VAULT_HCP_TOKEN
|
||||
VAULT_HCP_MOUNT_POINT = CONFIG.VAULT_HCP_MOUNT_POINT
|
||||
|
||||
VAULT_AZURE_HOST = CONFIG.VAULT_AZURE_HOST
|
||||
VAULT_AZURE_CLIENT_ID = CONFIG.VAULT_AZURE_CLIENT_ID
|
||||
VAULT_AZURE_CLIENT_SECRET = CONFIG.VAULT_AZURE_CLIENT_SECRET
|
||||
VAULT_AZURE_TENANT_ID = CONFIG.VAULT_AZURE_TENANT_ID
|
||||
|
||||
HISTORY_ACCOUNT_CLEAN_LIMIT = CONFIG.HISTORY_ACCOUNT_CLEAN_LIMIT
|
||||
|
||||
# Other setting
|
||||
|
@ -298,6 +305,11 @@ def get_file_md5(filepath):
|
|||
return m.hexdigest()
|
||||
|
||||
|
||||
# 人脸验证
|
||||
FACE_RECOGNITION_ENABLED = CONFIG.FACE_RECOGNITION_ENABLED
|
||||
FACE_RECOGNITION_DISTANCE_THRESHOLD = CONFIG.FACE_RECOGNITION_DISTANCE_THRESHOLD
|
||||
FACE_RECOGNITION_COSINE_THRESHOLD = CONFIG.FACE_RECOGNITION_COSINE_THRESHOLD
|
||||
|
||||
AUTH_CUSTOM = CONFIG.AUTH_CUSTOM
|
||||
AUTH_CUSTOM_FILE_MD5 = CONFIG.AUTH_CUSTOM_FILE_MD5
|
||||
AUTH_CUSTOM_FILE_PATH = os.path.join(PROJECT_DIR, 'data', 'auth', 'main.py')
|
||||
|
@ -306,11 +318,12 @@ if AUTH_CUSTOM and AUTH_CUSTOM_FILE_MD5 == get_file_md5(AUTH_CUSTOM_FILE_PATH):
|
|||
AUTHENTICATION_BACKENDS.append(AUTH_BACKEND_CUSTOM)
|
||||
|
||||
MFA_BACKEND_OTP = 'authentication.mfa.otp.MFAOtp'
|
||||
MFA_BACKEND_FACE = 'authentication.mfa.face.MFAFace'
|
||||
MFA_BACKEND_RADIUS = 'authentication.mfa.radius.MFARadius'
|
||||
MFA_BACKEND_SMS = 'authentication.mfa.sms.MFASms'
|
||||
MFA_BACKEND_CUSTOM = 'authentication.mfa.custom.MFACustom'
|
||||
|
||||
MFA_BACKENDS = [MFA_BACKEND_OTP, MFA_BACKEND_RADIUS, MFA_BACKEND_SMS]
|
||||
MFA_BACKENDS = [MFA_BACKEND_OTP, MFA_BACKEND_RADIUS, MFA_BACKEND_SMS, MFA_BACKEND_FACE]
|
||||
|
||||
MFA_CUSTOM = CONFIG.MFA_CUSTOM
|
||||
MFA_CUSTOM_FILE_MD5 = CONFIG.MFA_CUSTOM_FILE_MD5
|
||||
|
|
|
@ -116,7 +116,7 @@ EMAIL_CUSTOM_USER_CREATED_BODY = CONFIG.EMAIL_CUSTOM_USER_CREATED_BODY
|
|||
EMAIL_CUSTOM_USER_CREATED_SIGNATURE = CONFIG.EMAIL_CUSTOM_USER_CREATED_SIGNATURE
|
||||
|
||||
DISPLAY_PER_PAGE = CONFIG.DISPLAY_PER_PAGE
|
||||
DEFAULT_EXPIRED_YEARS = 70
|
||||
DEFAULT_EXPIRED_YEARS = CONFIG.DEFAULT_EXPIRED_YEARS
|
||||
USER_GUIDE_URL = CONFIG.USER_GUIDE_URL
|
||||
HTTP_LISTEN_PORT = CONFIG.HTTP_LISTEN_PORT
|
||||
WS_LISTEN_PORT = CONFIG.WS_LISTEN_PORT
|
||||
|
|
|
@ -101,7 +101,7 @@ class ResourceDownload(TemplateView):
|
|||
MRD_VERSION=10.6.7
|
||||
OPENSSH_VERSION=v9.4.0.0
|
||||
TINKER_VERSION=v0.1.6
|
||||
VIDEO_PLAYER_VERSION=0.1.9
|
||||
VIDEO_PLAYER_VERSION=0.2.0
|
||||
CLIENT_VERSION=v2.1.3
|
||||
"""
|
||||
|
||||
|
|
|
@ -127,13 +127,16 @@ class Message(metaclass=MessageType):
|
|||
def get_html_msg(self) -> dict:
|
||||
return self.get_common_msg()
|
||||
|
||||
def get_markdown_msg(self) -> dict:
|
||||
@staticmethod
|
||||
def html_to_markdown(html_msg):
|
||||
h = HTML2Text()
|
||||
h.body_width = 300
|
||||
msg = self.get_html_msg()
|
||||
content = msg['message']
|
||||
msg['message'] = h.handle(content)
|
||||
return msg
|
||||
h.body_width = 0
|
||||
content = html_msg['message']
|
||||
html_msg['message'] = h.handle(content)
|
||||
return html_msg
|
||||
|
||||
def get_markdown_msg(self) -> dict:
|
||||
return self.html_to_markdown(self.get_html_msg())
|
||||
|
||||
def get_text_msg(self) -> dict:
|
||||
h = HTML2Text()
|
||||
|
|
|
@ -78,7 +78,7 @@ class AdHocRunner:
|
|||
|
||||
|
||||
class PlaybookRunner:
|
||||
def __init__(self, inventory, playbook, project_dir='/tmp/', callback=None):
|
||||
def __init__(self, inventory, playbook, project_dir='/tmp/', callback=None, extra_vars=None, ):
|
||||
|
||||
self.id = uuid.uuid4()
|
||||
self.inventory = inventory
|
||||
|
@ -89,6 +89,9 @@ class PlaybookRunner:
|
|||
self.cb = callback
|
||||
self.isolate = True
|
||||
self.envs = {}
|
||||
if extra_vars is None:
|
||||
extra_vars = {}
|
||||
self.extra_vars = extra_vars
|
||||
|
||||
def copy_playbook(self):
|
||||
entry = os.path.basename(self.playbook)
|
||||
|
@ -119,6 +122,7 @@ class PlaybookRunner:
|
|||
status_handler=self.cb.status_handler,
|
||||
host_cwd=self.project_dir,
|
||||
envvars=self.envs,
|
||||
extravars=self.extra_vars,
|
||||
**kwargs
|
||||
)
|
||||
return self.cb
|
||||
|
|
|
@ -4,3 +4,4 @@ from .adhoc import *
|
|||
from .celery import *
|
||||
from .job import *
|
||||
from .playbook import *
|
||||
from .variable import *
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.api.generic import JMSBulkModelViewSet
|
||||
from common.utils.http import is_true
|
||||
|
@ -20,10 +21,15 @@ class AdHocViewSet(JMSBulkModelViewSet):
|
|||
search_fields = ('name', 'comment')
|
||||
filterset_fields = ['scope', 'creator']
|
||||
|
||||
def allow_bulk_destroy(self, qs, filtered):
|
||||
for obj in filtered:
|
||||
self.check_object_permissions(self.request, obj)
|
||||
return True
|
||||
|
||||
def check_object_permissions(self, request, obj):
|
||||
if request.method != 'GET' and obj.creator != request.user:
|
||||
self.permission_denied(
|
||||
request, message={"detail": "Deleting other people's script is not allowed"}
|
||||
request, message={"detail": _("Deleting other people's script is not allowed")}
|
||||
)
|
||||
return super().check_object_permissions(request, obj)
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ from ops.models import Job, JobExecution
|
|||
from ops.serializers.job import (
|
||||
JobSerializer, JobExecutionSerializer, FileSerializer, JobTaskStopSerializer
|
||||
)
|
||||
from ops.utils import merge_nodes_and_assets
|
||||
|
||||
__all__ = [
|
||||
'JobViewSet', 'JobExecutionViewSet', 'JobRunVariableHelpAPIView', 'JobExecutionTaskDetail', 'UsernameHintsAPI'
|
||||
|
@ -36,8 +37,6 @@ from accounts.models import Account
|
|||
from assets.const import Protocol
|
||||
from perms.const import ActionChoices
|
||||
from perms.utils.asset_perm import PermAssetDetailUtil
|
||||
from perms.models import PermNode
|
||||
from perms.utils import UserPermAssetUtil
|
||||
from jumpserver.settings import get_file_md5
|
||||
|
||||
|
||||
|
@ -47,26 +46,12 @@ def set_task_to_serializer_data(serializer, task_id):
|
|||
setattr(serializer, "_data", data)
|
||||
|
||||
|
||||
def merge_nodes_and_assets(nodes, assets, user):
|
||||
if not nodes:
|
||||
return assets
|
||||
perm_util = UserPermAssetUtil(user=user)
|
||||
for node_id in nodes:
|
||||
if node_id == PermNode.FAVORITE_NODE_KEY:
|
||||
node_assets = perm_util.get_favorite_assets()
|
||||
elif node_id == PermNode.UNGROUPED_NODE_KEY:
|
||||
node_assets = perm_util.get_ungroup_assets()
|
||||
else:
|
||||
_, node_assets = perm_util.get_node_all_assets(node_id)
|
||||
assets.extend(node_assets.exclude(id__in=[asset.id for asset in assets]))
|
||||
return assets
|
||||
|
||||
|
||||
class JobViewSet(OrgBulkModelViewSet):
|
||||
serializer_class = JobSerializer
|
||||
filterset_fields = ('name', 'type')
|
||||
search_fields = ('name', 'comment')
|
||||
model = Job
|
||||
_parameters = None
|
||||
|
||||
def check_permissions(self, request):
|
||||
# job: upload_file
|
||||
|
@ -106,10 +91,10 @@ class JobViewSet(OrgBulkModelViewSet):
|
|||
|
||||
def perform_create(self, serializer):
|
||||
run_after_save = serializer.validated_data.pop('run_after_save', False)
|
||||
node_ids = serializer.validated_data.pop('nodes', [])
|
||||
assets = serializer.validated_data.get('assets')
|
||||
assets = merge_nodes_and_assets(node_ids, assets, self.request.user)
|
||||
serializer.validated_data['assets'] = assets
|
||||
self._parameters = serializer.validated_data.pop('parameters', None)
|
||||
nodes = serializer.validated_data.pop('nodes', [])
|
||||
assets = serializer.validated_data.get('assets', [])
|
||||
assets = merge_nodes_and_assets(nodes, assets, self.request.user)
|
||||
if serializer.validated_data.get('type') == Types.upload_file:
|
||||
account_name = serializer.validated_data.get('runas')
|
||||
self.check_upload_permission(assets, account_name)
|
||||
|
@ -120,12 +105,15 @@ class JobViewSet(OrgBulkModelViewSet):
|
|||
|
||||
def perform_update(self, serializer):
|
||||
run_after_save = serializer.validated_data.pop('run_after_save', False)
|
||||
self._parameters = serializer.validated_data.pop('parameters', None)
|
||||
instance = serializer.save()
|
||||
if run_after_save:
|
||||
self.run_job(instance, serializer)
|
||||
|
||||
def run_job(self, job, serializer):
|
||||
execution = job.create_execution()
|
||||
if self._parameters:
|
||||
execution.parameters = JobExecutionSerializer.validate_parameters(self._parameters)
|
||||
execution.creator = self.request.user
|
||||
execution.save()
|
||||
|
||||
|
@ -238,7 +226,11 @@ class JobExecutionViewSet(OrgBulkModelViewSet):
|
|||
return Response({'error': serializer.errors}, status=400)
|
||||
task_id = serializer.validated_data['task_id']
|
||||
try:
|
||||
instance = get_object_or_404(JobExecution, pk=task_id, creator=request.user)
|
||||
user = request.user
|
||||
if user.has_perm("audits.view_joblog"):
|
||||
instance = get_object_or_404(JobExecution, task_id=task_id)
|
||||
else:
|
||||
instance = get_object_or_404(JobExecution, task_id=task_id, creator=request.user)
|
||||
except Http404:
|
||||
return Response(
|
||||
{'error': _('The task is being created and cannot be interrupted. Please try again later.')},
|
||||
|
@ -300,7 +292,7 @@ class UsernameHintsAPI(APIView):
|
|||
permission_classes = [IsValidUser]
|
||||
|
||||
def post(self, request, **kwargs):
|
||||
node_ids = request.data.get('nodes', None)
|
||||
node_ids = request.data.get('nodes', [])
|
||||
asset_ids = request.data.get('assets', [])
|
||||
query = request.data.get('query', None)
|
||||
|
||||
|
|
|
@ -38,10 +38,15 @@ class PlaybookViewSet(JMSBulkModelViewSet):
|
|||
search_fields = ('name', 'comment')
|
||||
filterset_fields = ['scope', 'creator']
|
||||
|
||||
def allow_bulk_destroy(self, qs, filtered):
|
||||
for obj in filtered:
|
||||
self.check_object_permissions(self.request, obj)
|
||||
return True
|
||||
|
||||
def check_object_permissions(self, request, obj):
|
||||
if request.method != 'GET' and obj.creator != request.user:
|
||||
self.permission_denied(
|
||||
request, message={"detail": "Deleting other people's playbook is not allowed"}
|
||||
request, message={"detail": _("Deleting other people's playbook is not allowed")}
|
||||
)
|
||||
return super().check_object_permissions(request, obj)
|
||||
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.api.generic import JMSModelViewSet
|
||||
from common.const.http import OPTIONS, GET
|
||||
from common.permissions import IsValidUser
|
||||
from ..models import Variable
|
||||
from ..serializers import VariableSerializer, VariableFormDataSerializer
|
||||
|
||||
__all__ = [
|
||||
'VariableViewSet'
|
||||
]
|
||||
|
||||
|
||||
class VariableViewSet(JMSModelViewSet):
|
||||
queryset = Variable.objects.all()
|
||||
serializer_class = VariableSerializer
|
||||
http_method_names = ['options', 'get']
|
||||
|
||||
@action(methods=[GET], detail=False, serializer_class=VariableFormDataSerializer,
|
||||
permission_classes=[IsValidUser, ], url_path='form_data')
|
||||
def form_data(self, request, *args, **kwargs):
|
||||
# 只是为了动态返回serializer fields info
|
||||
return Response({})
|
|
@ -85,3 +85,8 @@ COMMAND_EXECUTION_DISABLED = _('Command execution disabled')
|
|||
class Scope(models.TextChoices):
|
||||
public = 'public', pgettext_lazy("scope", 'Public')
|
||||
private = 'private', _('Private')
|
||||
|
||||
|
||||
class FieldType(models.TextChoices):
|
||||
text = 'text', _('Text')
|
||||
select = 'select', _('Select')
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 4.1.13 on 2024-10-21 08:02
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('assets', '0006_database_pg_ssl_mode'),
|
||||
('ops', '0003_alter_adhoc_unique_together_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='job',
|
||||
name='nodes',
|
||||
field=models.ManyToManyField(blank=True, to='assets.node', verbose_name='Node'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='job',
|
||||
name='assets',
|
||||
field=models.ManyToManyField(blank=True, to='assets.asset', verbose_name='Assets'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='job',
|
||||
unique_together={('name', 'org_id', 'creator', 'type')},
|
||||
),
|
||||
]
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue