diff --git a/Dockerfile b/Dockerfile
index de4022338..7e6eac422 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -17,6 +17,7 @@ ARG DEPENDENCIES=" \
libxmlsec1-dev \
libxmlsec1-openssl \
libaio-dev \
+ openssh-client \
sshpass"
ARG TOOLS=" \
@@ -29,24 +30,22 @@ ARG TOOLS=" \
redis-tools \
telnet \
vim \
- unzip \
+ unzip \
wget"
-RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
- && sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
- && apt update && sleep 1 && apt update \
- && apt -y install ${BUILD_DEPENDENCIES} \
- && apt -y install ${DEPENDENCIES} \
- && apt -y install ${TOOLS} \
+RUN sed -i 's@http://.*.debian.org@http://mirrors.ustc.edu.cn@g' /etc/apt/sources.list \
+ && apt-get update \
+ && apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
+ && apt-get -y install --no-install-recommends ${DEPENDENCIES} \
+ && apt-get -y install --no-install-recommends ${TOOLS} \
&& localedef -c -f UTF-8 -i zh_CN zh_CN.UTF-8 \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& mkdir -p /root/.ssh/ \
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
&& sed -i "s@# alias l@alias l@g" ~/.bashrc \
&& echo "set mouse-=a" > ~/.vimrc \
- && rm -rf /var/lib/apt/lists/* \
- && mv /bin/sh /bin/sh.bak \
- && ln -s /bin/bash /bin/sh
+ && echo "no" | dpkg-reconfigure dash \
+ && rm -rf /var/lib/apt/lists/*
ARG TARGETARCH
ARG ORACLE_LIB_MAJOR=19
@@ -65,9 +64,9 @@ RUN mkdir -p /opt/oracle/ \
WORKDIR /tmp/build
COPY ./requirements ./requirements
-ARG PIP_MIRROR=https://mirrors.aliyun.com/pypi/simple/
+ARG PIP_MIRROR=https://pypi.douban.com/simple
ENV PIP_MIRROR=$PIP_MIRROR
-ARG PIP_JMS_MIRROR=https://mirrors.aliyun.com/pypi/simple/
+ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
# 因为以 jms 或者 jumpserver 开头的 mirror 上可能没有
RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \
diff --git a/README.md b/README.md
index 70dac07d1..59c4e55cf 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@
-JumpServer 是全球首款开源的堡垒机,使用 GPLv3 开源协议,是符合 4A 规范的运维安全审计系统。
+JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。
JumpServer 使用 Python 开发,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
@@ -95,11 +95,15 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
### 案例研究
-- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147)
-- [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882)
-- [携程 JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851)
-- [小红书的JumpServer堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516)
-- [JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732)
+- [腾讯海外游戏:基于JumpServer构建游戏安全运营能力](https://blog.fit2cloud.com/?p=3704)
+- [万华化学:通过JumpServer管理全球化分布式IT资产,并且实现与云管平台的联动](https://blog.fit2cloud.com/?p=3504)
+- [雪花啤酒:JumpServer堡垒机使用体会](https://blog.fit2cloud.com/?p=3412)
+- [顺丰科技:JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147)
+- [沐瞳游戏:通过JumpServer管控多项目分布式资产](https://blog.fit2cloud.com/?p=3213)
+- [携程:JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851)
+- [大智慧:JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882)
+- [小红书:的JumpServer堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516)
+- [中手游:JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732)
- [中通快递:JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708)
- [东方明珠:JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687)
- [江苏农信:JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)
diff --git a/apps/acls/models/login_acl.py b/apps/acls/models/login_acl.py
index 1887ecd33..6fcff0231 100644
--- a/apps/acls/models/login_acl.py
+++ b/apps/acls/models/login_acl.py
@@ -44,58 +44,29 @@ class LoginACL(BaseACL):
def __str__(self):
return self.name
- @property
- def action_reject(self):
- return self.action == self.ActionChoices.reject
-
- @property
- def action_allow(self):
- return self.action == self.ActionChoices.allow
+ def is_action(self, action):
+ return self.action == action
@classmethod
def filter_acl(cls, user):
return user.login_acls.all().valid().distinct()
@staticmethod
- def allow_user_confirm_if_need(user, ip):
- acl = LoginACL.filter_acl(user).filter(
- action=LoginACL.ActionChoices.confirm
- ).first()
- acl = acl if acl and acl.reviewers.exists() else None
- if not acl:
- return False, acl
- ip_group = acl.rules.get('ip_group')
- time_periods = acl.rules.get('time_period')
- is_contain_ip = contains_ip(ip, ip_group)
- is_contain_time_period = contains_time_period(time_periods)
- return is_contain_ip and is_contain_time_period, acl
+ def match(user, ip):
+ acls = LoginACL.filter_acl(user)
+ if not acls:
+ return
- @staticmethod
- def allow_user_to_login(user, ip):
- acl = LoginACL.filter_acl(user).exclude(
- action=LoginACL.ActionChoices.confirm
- ).first()
- if not acl:
- return True, ''
- ip_group = acl.rules.get('ip_group')
- time_periods = acl.rules.get('time_period')
- is_contain_ip = contains_ip(ip, ip_group)
- is_contain_time_period = contains_time_period(time_periods)
-
- reject_type = ''
- if is_contain_ip and is_contain_time_period:
- # 满足条件
- allow = acl.action_allow
- if not allow:
- reject_type = 'ip' if is_contain_ip else 'time'
- else:
- # 不满足条件
- # 如果acl本身允许,那就拒绝;如果本身拒绝,那就允许
- allow = not acl.action_allow
- if not allow:
- reject_type = 'ip' if not is_contain_ip else 'time'
-
- return allow, reject_type
+ for acl in acls:
+ if acl.is_action(LoginACL.ActionChoices.confirm) and not acl.reviewers.exists():
+ continue
+ ip_group = acl.rules.get('ip_group')
+ time_periods = acl.rules.get('time_period')
+ is_contain_ip = contains_ip(ip, ip_group)
+ is_contain_time_period = contains_time_period(time_periods)
+ if is_contain_ip and is_contain_time_period:
+ # 满足条件,则返回
+ return acl
def create_confirm_ticket(self, request):
from tickets import const
diff --git a/apps/applications/const.py b/apps/applications/const.py
new file mode 100644
index 000000000..4e0d2fe50
--- /dev/null
+++ b/apps/applications/const.py
@@ -0,0 +1,91 @@
+# coding: utf-8
+#
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+
+
+class AppCategory(models.TextChoices):
+ db = 'db', _('Database')
+ remote_app = 'remote_app', _('Remote app')
+ cloud = 'cloud', 'Cloud'
+
+ @classmethod
+ def get_label(cls, category):
+ return dict(cls.choices).get(category, '')
+
+ @classmethod
+ def is_xpack(cls, category):
+ return category in ['remote_app']
+
+
+class AppType(models.TextChoices):
+ # db category
+ mysql = 'mysql', 'MySQL'
+ mariadb = 'mariadb', 'MariaDB'
+ oracle = 'oracle', 'Oracle'
+ pgsql = 'postgresql', 'PostgreSQL'
+ sqlserver = 'sqlserver', 'SQLServer'
+ redis = 'redis', 'Redis'
+ mongodb = 'mongodb', 'MongoDB'
+
+ # remote-app category
+ chrome = 'chrome', 'Chrome'
+ mysql_workbench = 'mysql_workbench', 'MySQL Workbench'
+ vmware_client = 'vmware_client', 'vSphere Client'
+ custom = 'custom', _('Custom')
+
+ # cloud category
+ k8s = 'k8s', 'Kubernetes'
+
+ @classmethod
+ def category_types_mapper(cls):
+ return {
+ AppCategory.db: [
+ cls.mysql, cls.mariadb, cls.oracle, cls.pgsql,
+ cls.sqlserver, cls.redis, cls.mongodb
+ ],
+ AppCategory.remote_app: [
+ cls.chrome, cls.mysql_workbench,
+ cls.vmware_client, cls.custom
+ ],
+ AppCategory.cloud: [cls.k8s]
+ }
+
+ @classmethod
+ def type_category_mapper(cls):
+ mapper = {}
+ for category, tps in cls.category_types_mapper().items():
+ for tp in tps:
+ mapper[tp] = category
+ return mapper
+
+ @classmethod
+ def get_label(cls, tp):
+ return dict(cls.choices).get(tp, '')
+
+ @classmethod
+ def db_types(cls):
+ return [tp.value for tp in cls.category_types_mapper()[AppCategory.db]]
+
+ @classmethod
+ def remote_app_types(cls):
+ return [tp.value for tp in cls.category_types_mapper()[AppCategory.remote_app]]
+
+ @classmethod
+ def cloud_types(cls):
+ return [tp.value for tp in cls.category_types_mapper()[AppCategory.cloud]]
+
+ @classmethod
+ def is_xpack(cls, tp):
+ tp_category_mapper = cls.type_category_mapper()
+ category = tp_category_mapper[tp]
+
+ if AppCategory.is_xpack(category):
+ return True
+ return tp in ['oracle', 'postgresql', 'sqlserver']
+
+
+class OracleVersion(models.TextChoices):
+ version_11g = '11g', '11g'
+ version_12c = '12c', '12c'
+ version_other = 'other', _('Other')
diff --git a/apps/applications/migrations/0022_auto_20220714_1046.py b/apps/applications/migrations/0022_auto_20220714_1046.py
new file mode 100644
index 000000000..8ecb1cbe6
--- /dev/null
+++ b/apps/applications/migrations/0022_auto_20220714_1046.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.2.12 on 2022-07-14 02:46
+
+from django.db import migrations
+
+
+def migrate_db_oracle_version_to_attrs(apps, schema_editor):
+ db_alias = schema_editor.connection.alias
+ model = apps.get_model("applications", "Application")
+ oracles = list(model.objects.using(db_alias).filter(type='oracle'))
+ for o in oracles:
+ o.attrs['version'] = '12c'
+ model.objects.using(db_alias).bulk_update(oracles, ['attrs'])
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('applications', '0021_auto_20220629_1826'),
+ ]
+
+ operations = [
+ migrations.RunPython(migrate_db_oracle_version_to_attrs)
+ ]
diff --git a/apps/applications/migrations/0023_auto_20220715_1556.py b/apps/applications/migrations/0023_auto_20220715_1556.py
new file mode 100644
index 000000000..03123efab
--- /dev/null
+++ b/apps/applications/migrations/0023_auto_20220715_1556.py
@@ -0,0 +1,48 @@
+# Generated by Django 3.1.14 on 2022-07-15 07:56
+import time
+from collections import defaultdict
+
+from django.db import migrations
+
+
+def migrate_account_dirty_data(apps, schema_editor):
+ db_alias = schema_editor.connection.alias
+ account_model = apps.get_model('applications', 'Account')
+
+ count = 0
+ bulk_size = 1000
+
+ while True:
+ accounts = account_model.objects.using(db_alias) \
+ .filter(org_id='')[count:count + bulk_size]
+
+ if not accounts:
+ break
+
+ accounts = list(accounts)
+ start = time.time()
+ for i in accounts:
+ if i.app:
+ org_id = i.app.org_id
+ elif i.systemuser:
+ org_id = i.systemuser.org_id
+ else:
+ org_id = ''
+ if org_id:
+ i.org_id = org_id
+
+ account_model.objects.bulk_update(accounts, ['org_id', ])
+ print("Update account org is empty: {}-{} using: {:.2f}s".format(
+ count, count + len(accounts), time.time() - start
+ ))
+ count += len(accounts)
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('applications', '0022_auto_20220714_1046'),
+ ]
+
+ operations = [
+ migrations.RunPython(migrate_account_dirty_data),
+ ]
diff --git a/apps/applications/models/application.py b/apps/applications/models/application.py
new file mode 100644
index 000000000..cc98abaf8
--- /dev/null
+++ b/apps/applications/models/application.py
@@ -0,0 +1,320 @@
+from collections import defaultdict
+from urllib.parse import urlencode, parse_qsl
+
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+from django.conf import settings
+
+from orgs.mixins.models import OrgModelMixin
+from common.mixins import CommonModelMixin
+from common.tree import TreeNode
+from common.utils import is_uuid
+from assets.models import Asset, SystemUser
+from ..const import OracleVersion
+
+from ..utils import KubernetesTree
+from .. import const
+
+
+class ApplicationTreeNodeMixin:
+ id: str
+ name: str
+ type: str
+ category: str
+ attrs: dict
+
+ @staticmethod
+ def create_tree_id(pid, type, v):
+ i = dict(parse_qsl(pid))
+ i[type] = v
+ tree_id = urlencode(i)
+ return tree_id
+
+ @classmethod
+ def create_choice_node(cls, c, id_, pid, tp, opened=False, counts=None,
+ show_empty=True, show_count=True):
+ count = counts.get(c.value, 0)
+ if count == 0 and not show_empty:
+ return None
+ label = c.label
+ if count is not None and show_count:
+ label = '{} ({})'.format(label, count)
+ data = {
+ 'id': id_,
+ 'name': label,
+ 'title': label,
+ 'pId': pid,
+ 'isParent': bool(count),
+ 'open': opened,
+ 'iconSkin': '',
+ 'meta': {
+ 'type': tp,
+ 'data': {
+ 'name': c.name,
+ 'value': c.value
+ }
+ }
+ }
+ return TreeNode(**data)
+
+ @classmethod
+ def create_root_tree_node(cls, queryset, show_count=True):
+ count = queryset.count() if show_count else None
+ root_id = 'applications'
+ root_name = _('Applications')
+ if count is not None and show_count:
+ root_name = '{} ({})'.format(root_name, count)
+ node = TreeNode(**{
+ 'id': root_id,
+ 'name': root_name,
+ 'title': root_name,
+ 'pId': '',
+ 'isParent': True,
+ 'open': True,
+ 'iconSkin': '',
+ 'meta': {
+ 'type': 'applications_root',
+ }
+ })
+ return node
+
+ @classmethod
+ def create_category_tree_nodes(cls, pid, counts=None, show_empty=True, show_count=True):
+ nodes = []
+ categories = const.AppType.category_types_mapper().keys()
+ for category in categories:
+ if not settings.XPACK_ENABLED and const.AppCategory.is_xpack(category):
+ continue
+ i = cls.create_tree_id(pid, 'category', category.value)
+ node = cls.create_choice_node(
+ category, i, pid=pid, tp='category',
+ counts=counts, opened=False, show_empty=show_empty,
+ show_count=show_count
+ )
+ if not node:
+ continue
+ nodes.append(node)
+ return nodes
+
+ @classmethod
+ def create_types_tree_nodes(cls, pid, counts, show_empty=True, show_count=True):
+ nodes = []
+ temp_pid = pid
+ type_category_mapper = const.AppType.type_category_mapper()
+ types = const.AppType.type_category_mapper().keys()
+
+ for tp in types:
+ if not settings.XPACK_ENABLED and const.AppType.is_xpack(tp):
+ continue
+ category = type_category_mapper.get(tp)
+ pid = cls.create_tree_id(pid, 'category', category.value)
+ i = cls.create_tree_id(pid, 'type', tp.value)
+ node = cls.create_choice_node(
+ tp, i, pid, tp='type', counts=counts, opened=False,
+ show_empty=show_empty, show_count=show_count
+ )
+ pid = temp_pid
+ if not node:
+ continue
+ nodes.append(node)
+ return nodes
+
+ @staticmethod
+ def get_tree_node_counts(queryset):
+ counts = defaultdict(int)
+ values = queryset.values_list('type', 'category')
+ for i in values:
+ tp = i[0]
+ category = i[1]
+ counts[tp] += 1
+ counts[category] += 1
+ return counts
+
+ @classmethod
+ def create_category_type_tree_nodes(cls, queryset, pid, show_empty=True, show_count=True):
+ counts = cls.get_tree_node_counts(queryset)
+ tree_nodes = []
+
+ # 类别的节点
+ tree_nodes += cls.create_category_tree_nodes(
+ pid, counts, show_empty=show_empty,
+ show_count=show_count
+ )
+
+ # 类型的节点
+ tree_nodes += cls.create_types_tree_nodes(
+ pid, counts, show_empty=show_empty,
+ show_count=show_count
+ )
+ return tree_nodes
+
+ @classmethod
+ def create_tree_nodes(cls, queryset, root_node=None, show_empty=True, show_count=True):
+ tree_nodes = []
+
+ # 根节点有可能是组织名称
+ if root_node is None:
+ root_node = cls.create_root_tree_node(queryset, show_count=show_count)
+ tree_nodes.append(root_node)
+
+ tree_nodes += cls.create_category_type_tree_nodes(
+ queryset, root_node.id, show_empty=show_empty, show_count=show_count
+ )
+
+ # 应用的节点
+ for app in queryset:
+ if not settings.XPACK_ENABLED and const.AppType.is_xpack(app.type):
+ continue
+ node = app.as_tree_node(root_node.id)
+ tree_nodes.append(node)
+ return tree_nodes
+
+ def create_app_tree_pid(self, root_id):
+ pid = self.create_tree_id(root_id, 'category', self.category)
+ pid = self.create_tree_id(pid, 'type', self.type)
+ return pid
+
+ def as_tree_node(self, pid, k8s_as_tree=False):
+ if self.type == const.AppType.k8s and k8s_as_tree:
+ node = KubernetesTree(pid).as_tree_node(self)
+ else:
+ node = self._as_tree_node(pid)
+ return node
+
+ def _attrs_to_tree(self):
+ if self.category == const.AppCategory.db:
+ return self.attrs
+ return {}
+
+ def _as_tree_node(self, pid):
+ icon_skin_category_mapper = {
+ 'remote_app': 'chrome',
+ 'db': 'database',
+ 'cloud': 'cloud'
+ }
+ icon_skin = icon_skin_category_mapper.get(self.category, 'file')
+ pid = self.create_app_tree_pid(pid)
+ node = TreeNode(**{
+ 'id': str(self.id),
+ 'name': self.name,
+ 'title': self.name,
+ 'pId': pid,
+ 'isParent': False,
+ 'open': False,
+ 'iconSkin': icon_skin,
+ 'meta': {
+ 'type': 'application',
+ 'data': {
+ 'category': self.category,
+ 'type': self.type,
+ 'attrs': self._attrs_to_tree()
+ }
+ }
+ })
+ return node
+
+
+class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
+ APP_TYPE = const.AppType
+
+ name = models.CharField(max_length=128, verbose_name=_('Name'))
+ category = models.CharField(
+ max_length=16, choices=const.AppCategory.choices, verbose_name=_('Category')
+ )
+ type = models.CharField(
+ max_length=16, choices=const.AppType.choices, verbose_name=_('Type')
+ )
+ domain = models.ForeignKey(
+ 'assets.Domain', null=True, blank=True, related_name='applications',
+ on_delete=models.SET_NULL, verbose_name=_("Domain"),
+ )
+ attrs = models.JSONField(default=dict, verbose_name=_('Attrs'))
+ comment = models.TextField(
+ max_length=128, default='', blank=True, verbose_name=_('Comment')
+ )
+
+ class Meta:
+ verbose_name = _('Application')
+ unique_together = [('org_id', 'name')]
+ ordering = ('name',)
+ permissions = [
+ ('match_application', _('Can match application')),
+ ]
+
+ def __str__(self):
+ category_display = self.get_category_display()
+ type_display = self.get_type_display()
+ return f'{self.name}({type_display})[{category_display}]'
+
+ @property
+ def category_remote_app(self):
+ return self.category == const.AppCategory.remote_app.value
+
+ @property
+ def category_cloud(self):
+ return self.category == const.AppCategory.cloud.value
+
+ @property
+ def category_db(self):
+ return self.category == const.AppCategory.db.value
+
+ def is_type(self, tp):
+ return self.type == tp
+
+ def get_rdp_remote_app_setting(self):
+ from applications.serializers.attrs import get_serializer_class_by_application_type
+ if not self.category_remote_app:
+ raise ValueError(f"Not a remote app application: {self.name}")
+ serializer_class = get_serializer_class_by_application_type(self.type)
+ fields = serializer_class().get_fields()
+
+ parameters = [self.type]
+ for field_name in list(fields.keys()):
+ if field_name in ['asset']:
+ continue
+ value = self.attrs.get(field_name)
+ if not value:
+ continue
+ if field_name == 'path':
+ value = '\"%s\"' % value
+ parameters.append(str(value))
+
+ parameters = ' '.join(parameters)
+ return {
+ 'program': '||jmservisor',
+ 'working_directory': '',
+ 'parameters': parameters
+ }
+
+ def get_remote_app_asset(self, raise_exception=True):
+ asset_id = self.attrs.get('asset')
+ if is_uuid(asset_id):
+ return Asset.objects.filter(id=asset_id).first()
+ if raise_exception:
+ raise ValueError("Remote App not has asset attr")
+
+ def get_target_ip(self):
+ target_ip = ''
+ if self.category_remote_app:
+ asset = self.get_remote_app_asset()
+ target_ip = asset.ip if asset else target_ip
+ elif self.category_cloud:
+ target_ip = self.attrs.get('cluster')
+ elif self.category_db:
+ target_ip = self.attrs.get('host')
+ return target_ip
+
+ def get_target_protocol_for_oracle(self):
+ """ Oracle 类型需要单独处理,因为要携带版本号 """
+ if not self.is_type(self.APP_TYPE.oracle):
+ return
+ version = self.attrs.get('version', OracleVersion.version_12c)
+ if version == OracleVersion.version_other:
+ return
+ return 'oracle_%s' % version
+
+
+class ApplicationUser(SystemUser):
+ class Meta:
+ proxy = True
+ verbose_name = _('Application user')
diff --git a/apps/applications/serializers/attrs/application_category/remote_app.py b/apps/applications/serializers/attrs/application_category/remote_app.py
new file mode 100644
index 000000000..063af6daa
--- /dev/null
+++ b/apps/applications/serializers/attrs/application_category/remote_app.py
@@ -0,0 +1,60 @@
+# coding: utf-8
+#
+
+from rest_framework import serializers
+from django.utils.translation import ugettext_lazy as _
+from django.core.exceptions import ObjectDoesNotExist
+
+from common.utils import get_logger, is_uuid, get_object_or_none
+from assets.models import Asset
+
+logger = get_logger(__file__)
+
+__all__ = ['RemoteAppSerializer']
+
+
+class ExistAssetPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
+
+ def to_internal_value(self, data):
+ instance = super().to_internal_value(data)
+ return str(instance.id)
+
+ def to_representation(self, _id):
+ # _id 是 instance.id
+ if self.pk_field is not None:
+ return self.pk_field.to_representation(_id)
+ # 解决删除资产后,远程应用更新页面会显示资产ID的问题
+ asset = get_object_or_none(Asset, id=_id)
+ if not asset:
+ return None
+ return _id
+
+
+class RemoteAppSerializer(serializers.Serializer):
+ asset_info = serializers.SerializerMethodField(label=_('Asset Info'))
+ asset = ExistAssetPrimaryKeyRelatedField(
+ queryset=Asset.objects, required=True, label=_("Asset"), allow_null=True
+ )
+ path = serializers.CharField(
+ max_length=128, label=_('Application path'), allow_null=True
+ )
+
+ def validate_asset(self, asset):
+ if not asset:
+ raise serializers.ValidationError(_('This field is required.'))
+ return asset
+
+ @staticmethod
+ def get_asset_info(obj):
+ asset_id = obj.get('asset')
+ if not asset_id or not is_uuid(asset_id):
+ return {}
+ try:
+ asset = Asset.objects.get(id=str(asset_id))
+ except ObjectDoesNotExist as e:
+ logger.error(e)
+ return {}
+ if not asset:
+ return {}
+ asset_info = {'id': str(asset.id), 'hostname': asset.hostname}
+ return asset_info
diff --git a/apps/applications/serializers/attrs/application_type/oracle.py b/apps/applications/serializers/attrs/application_type/oracle.py
new file mode 100644
index 000000000..fdc8016d2
--- /dev/null
+++ b/apps/applications/serializers/attrs/application_type/oracle.py
@@ -0,0 +1,16 @@
+from rest_framework import serializers
+from django.utils.translation import ugettext_lazy as _
+
+from ..application_category import DBSerializer
+from applications.const import OracleVersion
+
+__all__ = ['OracleSerializer']
+
+
+class OracleSerializer(DBSerializer):
+ version = serializers.ChoiceField(
+ choices=OracleVersion.choices, default=OracleVersion.version_12c,
+ allow_null=True, label=_('Version'),
+ help_text=_('Magnus currently supports only 11g and 12c connections')
+ )
+ port = serializers.IntegerField(default=1521, label=_('Port'), allow_null=True)
diff --git a/apps/assets/api/account_history.py b/apps/assets/api/account_history.py
index 6ca4fd349..e64d8189c 100644
--- a/apps/assets/api/account_history.py
+++ b/apps/assets/api/account_history.py
@@ -26,6 +26,17 @@ class AccountHistoryViewSet(AccountViewSet):
}
http_method_names = ['get', 'options']
+<<<<<<< HEAD
+=======
+ def get_queryset(self):
+ queryset = self.model.objects.all() \
+ .annotate(ip=F('asset__ip')) \
+ .annotate(hostname=F('asset__hostname')) \
+ .annotate(platform=F('asset__platform__name')) \
+ .annotate(protocols=F('asset__protocols'))
+ return queryset
+
+>>>>>>> origin
class AccountHistorySecretsViewSet(RecordViewLogMixin, AccountHistoryViewSet):
serializer_classes = {
diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py
index e69de29bb..1e7cbdfe8 100644
--- a/apps/assets/api/asset.py
+++ b/apps/assets/api/asset.py
@@ -0,0 +1,313 @@
+<<<<<<< HEAD
+=======
+# -*- coding: utf-8 -*-
+#
+from rest_framework.viewsets import ModelViewSet
+from rest_framework.generics import RetrieveAPIView, ListAPIView
+from django.shortcuts import get_object_or_404
+from django.db.models import Q
+
+from common.utils import get_logger, get_object_or_none
+from common.mixins.api import SuggestionMixin, RenderToJsonMixin
+from users.models import User, UserGroup
+from users.serializers import UserSerializer, UserGroupSerializer
+from users.filters import UserFilter
+from perms.models import AssetPermission
+from perms.serializers import AssetPermissionSerializer
+from perms.filters import AssetPermissionFilter
+from orgs.mixins.api import OrgBulkModelViewSet
+from orgs.mixins import generics
+from assets.api import FilterAssetByNodeMixin
+from ..models import Asset, Node, Platform, Gateway
+from .. import serializers
+from ..tasks import (
+ update_assets_hardware_info_manual, test_assets_connectivity_manual,
+ test_system_users_connectivity_a_asset, push_system_users_a_asset
+)
+from ..filters import FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend
+
+logger = get_logger(__file__)
+__all__ = [
+ 'AssetViewSet', 'AssetPlatformRetrieveApi',
+ 'AssetGatewayListApi', 'AssetPlatformViewSet',
+ 'AssetTaskCreateApi', 'AssetsTaskCreateApi',
+ 'AssetPermUserListApi', 'AssetPermUserPermissionsListApi',
+ 'AssetPermUserGroupListApi', 'AssetPermUserGroupPermissionsListApi',
+]
+
+
+class AssetViewSet(SuggestionMixin, FilterAssetByNodeMixin, OrgBulkModelViewSet):
+ """
+ API endpoint that allows Asset to be viewed or edited.
+ """
+ model = Asset
+ filterset_fields = {
+ 'hostname': ['exact'],
+ 'ip': ['exact'],
+ 'system_users__id': ['exact'],
+ 'platform__base': ['exact'],
+ 'is_active': ['exact'],
+ 'protocols': ['exact', 'icontains']
+ }
+ search_fields = ("hostname", "ip")
+ ordering_fields = ("hostname", "ip", "port", "cpu_cores")
+ ordering = ('hostname', )
+ serializer_classes = {
+ 'default': serializers.AssetSerializer,
+ 'suggestion': serializers.MiniAssetSerializer
+ }
+ rbac_perms = {
+ 'match': 'assets.match_asset'
+ }
+ extra_filter_backends = [FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend]
+
+ def set_assets_node(self, assets):
+ if not isinstance(assets, list):
+ assets = [assets]
+ node_id = self.request.query_params.get('node_id')
+ if not node_id:
+ return
+ node = get_object_or_none(Node, pk=node_id)
+ if not node:
+ return
+ node.assets.add(*assets)
+
+ def perform_create(self, serializer):
+ assets = serializer.save()
+ self.set_assets_node(assets)
+
+
+class AssetPlatformRetrieveApi(RetrieveAPIView):
+ queryset = Platform.objects.all()
+ serializer_class = serializers.PlatformSerializer
+ rbac_perms = {
+ 'retrieve': 'assets.view_gateway'
+ }
+
+ def get_object(self):
+ asset_pk = self.kwargs.get('pk')
+ asset = get_object_or_404(Asset, pk=asset_pk)
+ return asset.platform
+
+
+class AssetPlatformViewSet(ModelViewSet, RenderToJsonMixin):
+ queryset = Platform.objects.all()
+ serializer_class = serializers.PlatformSerializer
+ filterset_fields = ['name', 'base']
+ search_fields = ['name']
+
+ def check_object_permissions(self, request, obj):
+ if request.method.lower() in ['delete', 'put', 'patch'] and obj.internal:
+ self.permission_denied(
+ request, message={"detail": "Internal platform"}
+ )
+ return super().check_object_permissions(request, obj)
+
+
+class AssetsTaskMixin:
+
+ def perform_assets_task(self, serializer):
+ data = serializer.validated_data
+ action = data['action']
+ assets = data.get('assets', [])
+ if action == "refresh":
+ task = update_assets_hardware_info_manual.delay(assets)
+ else:
+ # action == 'test':
+ task = test_assets_connectivity_manual.delay(assets)
+ return task
+
+ def perform_create(self, serializer):
+ task = self.perform_assets_task(serializer)
+ self.set_task_to_serializer_data(serializer, task)
+
+ def set_task_to_serializer_data(self, serializer, task):
+ data = getattr(serializer, '_data', {})
+ data["task"] = task.id
+ setattr(serializer, '_data', data)
+
+
+class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
+ model = Asset
+ serializer_class = serializers.AssetTaskSerializer
+
+ def create(self, request, *args, **kwargs):
+ pk = self.kwargs.get('pk')
+ request.data['asset'] = pk
+ request.data['assets'] = [pk]
+ return super().create(request, *args, **kwargs)
+
+ def check_permissions(self, request):
+ action = request.data.get('action')
+ action_perm_require = {
+ 'refresh': 'assets.refresh_assethardwareinfo',
+ 'push_system_user': 'assets.push_assetsystemuser',
+ 'test': 'assets.test_assetconnectivity',
+ 'test_system_user': 'assets.test_assetconnectivity'
+ }
+ perm_required = action_perm_require.get(action)
+ has = self.request.user.has_perm(perm_required)
+
+ if not has:
+ self.permission_denied(request)
+
+ def perform_asset_task(self, serializer):
+ data = serializer.validated_data
+ action = data['action']
+ if action not in ['push_system_user', 'test_system_user']:
+ return
+
+ asset = data['asset']
+ system_users = data.get('system_users')
+ if not system_users:
+ system_users = asset.get_all_system_users()
+ if action == 'push_system_user':
+ task = push_system_users_a_asset.delay(system_users, asset=asset)
+ elif action == 'test_system_user':
+ task = test_system_users_connectivity_a_asset.delay(system_users, asset=asset)
+ else:
+ task = None
+ return task
+
+ def perform_create(self, serializer):
+ task = self.perform_asset_task(serializer)
+ if not task:
+ task = self.perform_assets_task(serializer)
+ self.set_task_to_serializer_data(serializer, task)
+
+
+class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
+ model = Asset
+ serializer_class = serializers.AssetsTaskSerializer
+
+ def check_permissions(self, request):
+ action = request.data.get('action')
+ action_perm_require = {
+ 'refresh': 'assets.refresh_assethardwareinfo',
+ }
+ perm_required = action_perm_require.get(action)
+ has = self.request.user.has_perm(perm_required)
+ if not has:
+ self.permission_denied(request)
+
+
+class AssetGatewayListApi(generics.ListAPIView):
+ serializer_class = serializers.GatewayWithAuthSerializer
+ rbac_perms = {
+ 'list': 'assets.view_gateway'
+ }
+
+ def get_queryset(self):
+ asset_id = self.kwargs.get('pk')
+ asset = get_object_or_404(Asset, pk=asset_id)
+ if not asset.domain:
+ return Gateway.objects.none()
+ queryset = asset.domain.gateways.filter(protocol='ssh')
+ return queryset
+
+
+class BaseAssetPermUserOrUserGroupListApi(ListAPIView):
+ rbac_perms = {
+ 'GET': 'perms.view_assetpermission'
+ }
+
+ def get_object(self):
+ asset_id = self.kwargs.get('pk')
+ asset = get_object_or_404(Asset, pk=asset_id)
+ return asset
+
+ def get_asset_related_perms(self):
+ asset = self.get_object()
+ nodes = asset.get_all_nodes(flat=True)
+ perms = AssetPermission.objects.filter(Q(assets=asset) | Q(nodes__in=nodes))
+ return perms
+
+
+class AssetPermUserListApi(BaseAssetPermUserOrUserGroupListApi):
+ filterset_class = UserFilter
+ search_fields = ('username', 'email', 'name', 'id', 'source', 'role')
+ serializer_class = UserSerializer
+ rbac_perms = {
+ 'GET': 'perms.view_assetpermission'
+ }
+
+ def get_queryset(self):
+ perms = self.get_asset_related_perms()
+ users = User.objects.filter(
+ Q(assetpermissions__in=perms) | Q(groups__assetpermissions__in=perms)
+ ).distinct()
+ return users
+
+
+class AssetPermUserGroupListApi(BaseAssetPermUserOrUserGroupListApi):
+ serializer_class = UserGroupSerializer
+
+ def get_queryset(self):
+ perms = self.get_asset_related_perms()
+ user_groups = UserGroup.objects.filter(assetpermissions__in=perms).distinct()
+ return user_groups
+
+
+class BaseAssetPermUserOrUserGroupPermissionsListApiMixin(generics.ListAPIView):
+ model = AssetPermission
+ serializer_class = AssetPermissionSerializer
+ filterset_class = AssetPermissionFilter
+ search_fields = ('name',)
+ rbac_perms = {
+ 'list': 'perms.view_assetpermission'
+ }
+
+ def get_object(self):
+ asset_id = self.kwargs.get('pk')
+ asset = get_object_or_404(Asset, pk=asset_id)
+ return asset
+
+ def filter_asset_related(self, queryset):
+ asset = self.get_object()
+ nodes = asset.get_all_nodes(flat=True)
+ perms = queryset.filter(Q(assets=asset) | Q(nodes__in=nodes))
+ return perms
+
+ def filter_queryset(self, queryset):
+ queryset = super().filter_queryset(queryset)
+ queryset = self.filter_asset_related(queryset)
+ return queryset
+
+
+class AssetPermUserPermissionsListApi(BaseAssetPermUserOrUserGroupPermissionsListApiMixin):
+ def filter_queryset(self, queryset):
+ queryset = super().filter_queryset(queryset)
+ queryset = self.filter_user_related(queryset)
+ queryset = queryset.distinct()
+ return queryset
+
+ def filter_user_related(self, queryset):
+ user = self.get_perm_user()
+ user_groups = user.groups.all()
+ perms = queryset.filter(Q(users=user) | Q(user_groups__in=user_groups))
+ return perms
+
+ def get_perm_user(self):
+ user_id = self.kwargs.get('perm_user_id')
+ user = get_object_or_404(User, pk=user_id)
+ return user
+
+
+class AssetPermUserGroupPermissionsListApi(BaseAssetPermUserOrUserGroupPermissionsListApiMixin):
+ def filter_queryset(self, queryset):
+ queryset = super().filter_queryset(queryset)
+ queryset = self.filter_user_group_related(queryset)
+ queryset = queryset.distinct()
+ return queryset
+
+ def filter_user_group_related(self, queryset):
+ user_group = self.get_perm_user_group()
+ perms = queryset.filter(user_groups=user_group)
+ return perms
+
+ def get_perm_user_group(self):
+ user_group_id = self.kwargs.get('perm_user_group_id')
+ user_group = get_object_or_404(UserGroup, pk=user_group_id)
+ return user_group
+
+>>>>>>> origin
diff --git a/apps/assets/api/mixin.py b/apps/assets/api/mixin.py
index 3a8ccf396..ff69562bb 100644
--- a/apps/assets/api/mixin.py
+++ b/apps/assets/api/mixin.py
@@ -24,7 +24,7 @@ class SerializeToTreeNodeMixin:
'title': _name(node),
'pId': node.parent_key,
'isParent': True,
- 'open': node.is_org_root(),
+ 'open': True,
'meta': {
'data': {
"id": node.id,
diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py
index 24b5bf04e..45b2b27bf 100644
--- a/apps/assets/api/node.py
+++ b/apps/assets/api/node.py
@@ -44,7 +44,7 @@ __all__ = [
class NodeViewSet(SuggestionMixin, OrgBulkModelViewSet):
model = Node
filterset_fields = ('value', 'key', 'id')
- search_fields = ('value',)
+ search_fields = ('full_value',)
serializer_class = serializers.NodeSerializer
rbac_perms = {
'match': 'assets.match_node',
@@ -102,6 +102,8 @@ class NodeListAsTreeApi(generics.ListAPIView):
class NodeChildrenApi(generics.ListCreateAPIView):
serializer_class = serializers.NodeSerializer
+ search_fields = ('value',)
+
instance = None
is_initial = False
@@ -180,8 +182,15 @@ class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi):
"""
model = Node
+ def filter_queryset(self, queryset):
+ if not self.request.GET.get('search'):
+ return queryset
+ queryset = super().filter_queryset(queryset)
+ queryset = self.model.get_ancestor_queryset(queryset)
+ return queryset
+
def list(self, request, *args, **kwargs):
- nodes = self.get_queryset().order_by('value')
+ nodes = self.filter_queryset(self.get_queryset()).order_by('value')
nodes = self.serialize_nodes(nodes, with_asset_amount=True)
assets = self.get_assets()
data = [*nodes, *assets]
diff --git a/apps/assets/models/authbook.py b/apps/assets/models/authbook.py
new file mode 100644
index 000000000..a6b52927b
--- /dev/null
+++ b/apps/assets/models/authbook.py
@@ -0,0 +1,138 @@
+# -*- coding: utf-8 -*-
+#
+
+from django.db import models
+from django.db.models import F
+from django.utils.translation import ugettext_lazy as _
+from simple_history.models import HistoricalRecords
+
+from common.utils import lazyproperty, get_logger
+from .base import BaseUser, AbsConnectivity
+
+logger = get_logger(__name__)
+
+
+__all__ = ['AuthBook']
+
+
+class AuthBook(BaseUser, AbsConnectivity):
+ asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset'))
+ systemuser = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE, null=True, verbose_name=_("System user"))
+ version = models.IntegerField(default=1, verbose_name=_('Version'))
+ history = HistoricalRecords()
+
+ auth_attrs = ['username', 'password', 'private_key', 'public_key']
+
+ class Meta:
+ verbose_name = _('AuthBook')
+ unique_together = [('username', 'asset', 'systemuser')]
+ permissions = [
+ ('test_authbook', _('Can test asset account connectivity')),
+ ('view_assetaccountsecret', _('Can view asset account secret')),
+ ('change_assetaccountsecret', _('Can change asset account secret')),
+ ('view_assethistoryaccount', _('Can view asset history account')),
+ ('view_assethistoryaccountsecret', _('Can view asset history account secret')),
+ ]
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.auth_snapshot = {}
+
+ def get_or_systemuser_attr(self, attr):
+ val = getattr(self, attr, None)
+ if val:
+ return val
+ if self.systemuser:
+ return getattr(self.systemuser, attr, '')
+ return ''
+
+ def load_auth(self):
+ for attr in self.auth_attrs:
+ value = self.get_or_systemuser_attr(attr)
+ self.auth_snapshot[attr] = [getattr(self, attr), value]
+ setattr(self, attr, value)
+
+ def unload_auth(self):
+ if not self.systemuser:
+ return
+
+ for attr, values in self.auth_snapshot.items():
+ origin_value, loaded_value = values
+ current_value = getattr(self, attr, '')
+ if current_value == loaded_value:
+ setattr(self, attr, origin_value)
+
+ def save(self, *args, **kwargs):
+ self.unload_auth()
+ instance = super().save(*args, **kwargs)
+ self.load_auth()
+ return instance
+
+ @property
+ def username_display(self):
+ return self.get_or_systemuser_attr('username') or '*'
+
+ @lazyproperty
+ def systemuser_display(self):
+ if not self.systemuser:
+ return ''
+ return str(self.systemuser)
+
+ @property
+ def smart_name(self):
+ username = self.username_display
+
+ if self.asset:
+ asset = str(self.asset)
+ else:
+ asset = '*'
+ return '{}@{}'.format(username, asset)
+
+ def sync_to_system_user_account(self):
+ if self.systemuser:
+ return
+ matched = AuthBook.objects.filter(
+ asset=self.asset, systemuser__username=self.username
+ )
+ if not matched:
+ return
+
+ for i in matched:
+ i.password = self.password
+ i.private_key = self.private_key
+ i.public_key = self.public_key
+ i.comment = 'Update triggered by account {}'.format(self.id)
+
+ # 不触发post_save信号
+ self.__class__.objects.bulk_update(matched, fields=['password', 'private_key', 'public_key'])
+
+ def remove_asset_admin_user_if_need(self):
+ if not self.asset or not self.systemuser:
+ return
+ if not self.systemuser.is_admin_user or self.asset.admin_user != self.systemuser:
+ return
+ self.asset.admin_user = None
+ self.asset.save()
+ logger.debug('Remove asset admin user: {} {}'.format(self.asset, self.systemuser))
+
+ def update_asset_admin_user_if_need(self):
+ if not self.asset or not self.systemuser:
+ return
+ if not self.systemuser.is_admin_user or self.asset.admin_user == self.systemuser:
+ return
+ self.asset.admin_user = self.systemuser
+ self.asset.save()
+ logger.debug('Update asset admin user: {} {}'.format(self.asset, self.systemuser))
+
+ @classmethod
+ def get_queryset(cls):
+ queryset = cls.objects.all() \
+ .annotate(ip=F('asset__ip')) \
+ .annotate(hostname=F('asset__hostname')) \
+ .annotate(platform=F('asset__platform__name')) \
+ .annotate(protocols=F('asset__protocols'))
+ return queryset
+
+ def __str__(self):
+ return self.smart_name
+
diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py
index dcebab3eb..0e98bce14 100644
--- a/apps/assets/models/node.py
+++ b/apps/assets/models/node.py
@@ -25,7 +25,6 @@ from orgs.mixins.models import OrgModelMixin, OrgManager
from orgs.utils import get_current_org, tmp_to_org, tmp_to_root_org
from orgs.models import Organization
-
__all__ = ['Node', 'FamilyMixin', 'compute_parent_key', 'NodeQuerySet']
logger = get_logger(__name__)
@@ -98,6 +97,14 @@ class FamilyMixin:
q |= Q(key=self.key)
return Node.objects.filter(q)
+ @classmethod
+ def get_ancestor_queryset(cls, queryset, with_self=True):
+ parent_keys = set()
+ for i in queryset:
+ parent_keys.update(set(i.get_ancestor_keys(with_self=with_self)))
+ queryset = queryset.model.objects.filter(key__in=list(parent_keys)).distinct()
+ return queryset
+
@property
def children(self):
return self.get_children(with_self=False)
@@ -396,7 +403,7 @@ class NodeAllAssetsMappingMixin:
mapping[ancestor_key].update(asset_ids)
t3 = time.time()
- logger.info('t1-t2(DB Query): {} s, t3-t2(Generate mapping): {} s'.format(t2-t1, t3-t2))
+ logger.info('t1-t2(DB Query): {} s, t3-t2(Generate mapping): {} s'.format(t2 - t1, t3 - t2))
return mapping
diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py
new file mode 100644
index 000000000..5211cfef6
--- /dev/null
+++ b/apps/assets/serializers/asset.py
@@ -0,0 +1,226 @@
+# -*- coding: utf-8 -*-
+#
+from rest_framework import serializers
+from django.core.validators import RegexValidator
+from django.utils.translation import ugettext_lazy as _
+
+from orgs.mixins.serializers import BulkOrgResourceModelSerializer
+from ..models import Asset, Node, Platform, SystemUser
+
+__all__ = [
+ 'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer',
+ 'ProtocolsField', 'PlatformSerializer',
+ 'AssetTaskSerializer', 'AssetsTaskSerializer', 'ProtocolsField',
+]
+
+
+class ProtocolField(serializers.RegexField):
+ protocols = '|'.join(dict(Asset.Protocol.choices).keys())
+ default_error_messages = {
+ 'invalid': _('Protocol format should {}/{}').format(protocols, '1-65535')
+ }
+ regex = r'^(%s)/(\d{1,5})$' % protocols
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(self.regex, **kwargs)
+
+
+def validate_duplicate_protocols(values):
+ errors = []
+ names = []
+
+ for value in values:
+ if not value or '/' not in value:
+ continue
+ name = value.split('/')[0]
+ if name in names:
+ errors.append(_("Protocol duplicate: {}").format(name))
+ names.append(name)
+ errors.append('')
+ if any(errors):
+ raise serializers.ValidationError(errors)
+
+
+class ProtocolsField(serializers.ListField):
+ default_validators = [validate_duplicate_protocols]
+
+ def __init__(self, *args, **kwargs):
+ kwargs['child'] = ProtocolField()
+ kwargs['allow_null'] = True
+ kwargs['allow_empty'] = True
+ kwargs['min_length'] = 1
+ kwargs['max_length'] = 4
+ super().__init__(*args, **kwargs)
+
+ def to_representation(self, value):
+ if not value:
+ return []
+ return value.split(' ')
+
+
+class AssetSerializer(BulkOrgResourceModelSerializer):
+ platform = serializers.SlugRelatedField(
+ slug_field='name', queryset=Platform.objects.all(), label=_("Platform")
+ )
+ protocols = ProtocolsField(label=_('Protocols'), required=False, default=['ssh/22'])
+ domain_display = serializers.ReadOnlyField(source='domain.name', label=_('Domain name'))
+ nodes_display = serializers.ListField(
+ child=serializers.CharField(), label=_('Nodes name'), required=False
+ )
+ labels_display = serializers.ListField(
+ child=serializers.CharField(), label=_('Labels name'), required=False, read_only=True
+ )
+
+ """
+ 资产的数据结构
+ """
+
+ class Meta:
+ model = Asset
+ fields_mini = ['id', 'hostname', 'ip', 'platform', 'protocols']
+ fields_small = fields_mini + [
+ 'protocol', 'port', 'protocols', 'is_active',
+ 'public_ip', 'number', 'comment',
+ ]
+ fields_hardware = [
+ 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count',
+ 'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info',
+ 'os', 'os_version', 'os_arch', 'hostname_raw',
+ 'cpu_info', 'hardware_info',
+ ]
+ fields_fk = [
+ 'domain', 'domain_display', 'platform', 'admin_user', 'admin_user_display'
+ ]
+ fields_m2m = [
+ 'nodes', 'nodes_display', 'labels', 'labels_display',
+ ]
+ read_only_fields = [
+ 'connectivity', 'date_verified', 'cpu_info', 'hardware_info',
+ 'created_by', 'date_created',
+ ]
+ fields = fields_small + fields_hardware + fields_fk + fields_m2m + read_only_fields
+ extra_kwargs = {
+ 'protocol': {'write_only': True},
+ 'port': {'write_only': True},
+ 'hardware_info': {'label': _('Hardware info'), 'read_only': True},
+ 'admin_user_display': {'label': _('Admin user display'), 'read_only': True},
+ 'cpu_info': {'label': _('CPU info')},
+ }
+
+ def get_fields(self):
+ fields = super().get_fields()
+
+ admin_user_field = fields.get('admin_user')
+ # 因为 mixin 中对 fields 有处理,可能不需要返回 admin_user
+ if admin_user_field:
+ admin_user_field.queryset = SystemUser.objects.filter(type=SystemUser.Type.admin)
+ return fields
+
+ @classmethod
+ def setup_eager_loading(cls, queryset):
+ """ Perform necessary eager loading of data. """
+ queryset = queryset.prefetch_related('domain', 'platform', 'admin_user')
+ queryset = queryset.prefetch_related('nodes', 'labels')
+ return queryset
+
+ def compatible_with_old_protocol(self, validated_data):
+ protocols_data = validated_data.pop("protocols", [])
+
+ # 兼容老的api
+ name = validated_data.get("protocol")
+ port = validated_data.get("port")
+ if not protocols_data and name and port:
+ protocols_data.insert(0, '/'.join([name, str(port)]))
+ elif not name and not port and protocols_data:
+ protocol = protocols_data[0].split('/')
+ validated_data["protocol"] = protocol[0]
+ validated_data["port"] = int(protocol[1])
+ if protocols_data:
+ validated_data["protocols"] = ' '.join(protocols_data)
+
+ def perform_nodes_display_create(self, instance, nodes_display):
+ if not nodes_display:
+ return
+ nodes_to_set = []
+ for full_value in nodes_display:
+ node = Node.objects.filter(full_value=full_value).first()
+ if node:
+ nodes_to_set.append(node)
+ else:
+ node = Node.create_node_by_full_value(full_value)
+ nodes_to_set.append(node)
+ instance.nodes.set(nodes_to_set)
+
+ def create(self, validated_data):
+ self.compatible_with_old_protocol(validated_data)
+ nodes_display = validated_data.pop('nodes_display', '')
+ instance = super().create(validated_data)
+ self.perform_nodes_display_create(instance, nodes_display)
+ return instance
+
+ def update(self, instance, validated_data):
+ nodes_display = validated_data.pop('nodes_display', '')
+ self.compatible_with_old_protocol(validated_data)
+ instance = super().update(instance, validated_data)
+ self.perform_nodes_display_create(instance, nodes_display)
+ return instance
+
+
+class MiniAssetSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Asset
+ fields = AssetSerializer.Meta.fields_mini
+
+
+class PlatformSerializer(serializers.ModelSerializer):
+ meta = serializers.DictField(required=False, allow_null=True, label=_('Meta'))
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # TODO 修复 drf SlugField RegexValidator bug,之后记得删除
+ validators = self.fields['name'].validators
+ if isinstance(validators[-1], RegexValidator):
+ validators.pop()
+
+ class Meta:
+ model = Platform
+ fields = [
+ 'id', 'name', 'base', 'charset',
+ 'internal', 'meta', 'comment'
+ ]
+ extra_kwargs = {
+ 'internal': {'read_only': True},
+ }
+
+
+class AssetSimpleSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Asset
+ fields = ['id', 'hostname', 'ip', 'port', 'connectivity', 'date_verified']
+
+
+class AssetsTaskSerializer(serializers.Serializer):
+ ACTION_CHOICES = (
+ ('refresh', 'refresh'),
+ ('test', 'test'),
+ )
+ task = serializers.CharField(read_only=True)
+ action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
+ assets = serializers.PrimaryKeyRelatedField(
+ queryset=Asset.objects, required=False, allow_empty=True, many=True
+ )
+
+
+class AssetTaskSerializer(AssetsTaskSerializer):
+ ACTION_CHOICES = tuple(list(AssetsTaskSerializer.ACTION_CHOICES) + [
+ ('push_system_user', 'push_system_user'),
+ ('test_system_user', 'test_system_user')
+ ])
+ action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
+ asset = serializers.PrimaryKeyRelatedField(
+ queryset=Asset.objects, required=False, allow_empty=True, many=False
+ )
+ system_users = serializers.PrimaryKeyRelatedField(
+ queryset=SystemUser.objects, required=False, allow_empty=True, many=True
+ )
diff --git a/apps/audits/api.py b/apps/audits/api.py
index 6df4ec861..bef3f3a05 100644
--- a/apps/audits/api.py
+++ b/apps/audits/api.py
@@ -12,6 +12,7 @@ from common.api import CommonGenericViewSet
from orgs.mixins.api import OrgGenericViewSet, OrgBulkModelViewSet, OrgRelationMixin
from orgs.utils import current_org
from ops.models import CommandExecution
+from . import filters
from .models import FTPLog, UserLoginLog, OperateLog, PasswordChangeLog
from .serializers import FTPLogSerializer, UserLoginLogSerializer, CommandExecutionSerializer
from .serializers import OperateLogSerializer, PasswordChangeLogSerializer, CommandExecutionHostsRelationSerializer
@@ -128,10 +129,15 @@ class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet):
class CommandExecutionHostRelationViewSet(OrgRelationMixin, OrgBulkModelViewSet):
serializer_class = CommandExecutionHostsRelationSerializer
m2m_field = CommandExecution.hosts.field
+<<<<<<< HEAD
filterset_fields = [
'id', 'asset', 'commandexecution'
]
search_fields = ('asset__name', )
+=======
+ filterset_class = filters.CommandExecutionFilter
+ search_fields = ('asset__hostname', )
+>>>>>>> origin
http_method_names = ['options', 'get']
rbac_perms = {
'GET': 'ops.view_commandexecution',
diff --git a/apps/audits/filters.py b/apps/audits/filters.py
index 470c2c4b5..b8bf466ec 100644
--- a/apps/audits/filters.py
+++ b/apps/audits/filters.py
@@ -1,10 +1,14 @@
+from django.db.models import F, Value
+from django.db.models.functions import Concat
+from django_filters.rest_framework import CharFilter
from rest_framework import filters
from rest_framework.compat import coreapi, coreschema
from orgs.utils import current_org
+from ops.models import CommandExecution
+from common.drf.filters import BaseFilterSet
-
-__all__ = ['CurrentOrgMembersFilter']
+__all__ = ['CurrentOrgMembersFilter', 'CommandExecutionFilter']
class CurrentOrgMembersFilter(filters.BaseFilterBackend):
@@ -30,3 +34,22 @@ class CurrentOrgMembersFilter(filters.BaseFilterBackend):
else:
queryset = queryset.filter(user__in=self._get_user_list())
return queryset
+
+
+class CommandExecutionFilter(BaseFilterSet):
+ hostname_ip = CharFilter(method='filter_hostname_ip')
+
+ class Meta:
+ model = CommandExecution.hosts.through
+ fields = (
+ 'id', 'asset', 'commandexecution', 'hostname_ip'
+ )
+
+ def filter_hostname_ip(self, queryset, name, value):
+ queryset = queryset.annotate(
+ hostname_ip=Concat(
+ F('asset__hostname'), Value('('),
+ F('asset__ip'), Value(')')
+ )
+ ).filter(hostname_ip__icontains=value)
+ return queryset
diff --git a/apps/audits/tasks.py b/apps/audits/tasks.py
index 10fb67f44..5b58c5fd2 100644
--- a/apps/audits/tasks.py
+++ b/apps/audits/tasks.py
@@ -29,7 +29,7 @@ def clean_ftp_log_period():
now = timezone.now()
days = get_log_keep_day('FTP_LOG_KEEP_DAYS')
expired_day = now - datetime.timedelta(days=days)
- FTPLog.objects.filter(datetime__lt=expired_day).delete()
+ FTPLog.objects.filter(date_start__lt=expired_day).delete()
@register_as_period_task(interval=3600*24)
diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py
index 8ab5ecee9..22d9e9b04 100644
--- a/apps/authentication/api/connection_token.py
+++ b/apps/authentication/api/connection_token.py
@@ -1,3 +1,4 @@
+import abc
import os
import json
import base64
@@ -16,12 +17,11 @@ from orgs.mixins.api import RootOrgViewMixin
from perms.models import Action
from terminal.models import EndpointRule
from ..serializers import (
- ConnectionTokenSerializer, ConnectionTokenSecretSerializer, SuperConnectionTokenSerializer,
- ConnectionTokenDisplaySerializer,
+ ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
+ SuperConnectionTokenSerializer, ConnectionTokenDisplaySerializer,
)
from ..models import ConnectionToken
-
__all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet']
@@ -34,9 +34,12 @@ class ConnectionTokenMixin:
if not is_valid:
raise PermissionDenied(error)
- @staticmethod
- def get_request_resources(serializer):
- user = serializer.validated_data.get('user')
+ @abc.abstractmethod
+ def get_request_resource_user(self, serializer):
+ raise NotImplementedError
+
+ def get_request_resources(self, serializer):
+ user = self.get_request_resource_user(serializer)
asset = serializer.validated_data.get('asset')
application = serializer.validated_data.get('application')
system_user = serializer.validated_data.get('system_user')
@@ -164,9 +167,8 @@ class ConnectionTokenMixin:
rdp_options['remoteapplicationname:s'] = name
else:
name = '*'
-
- filename = "{}-{}-jumpserver".format(token.user.username, name)
- filename = urllib.parse.quote(filename)
+ prefix_name = f'{token.user.username}-{name}'
+ filename = self.get_connect_filename(prefix_name)
content = ''
for k, v in rdp_options.items():
@@ -174,6 +176,15 @@ class ConnectionTokenMixin:
return filename, content
+ @staticmethod
+ def get_connect_filename(prefix_name):
+ prefix_name = prefix_name.replace('/', '_')
+ prefix_name = prefix_name.replace('\\', '_')
+ prefix_name = prefix_name.replace('.', '_')
+ filename = f'{prefix_name}-jumpserver'
+ filename = urllib.parse.quote(filename)
+ return filename
+
def get_ssh_token(self, token: ConnectionToken):
if token.asset:
name = token.asset.name
@@ -181,7 +192,8 @@ class ConnectionTokenMixin:
name = token.application.name
else:
name = '*'
- filename = f'{token.user.username}-{name}-jumpserver'
+ prefix_name = f'{token.user.username}-{name}'
+ filename = self.get_connect_filename(prefix_name)
endpoint = self.get_smart_endpoint(
protocol='ssh', asset=token.asset, application=token.application
@@ -198,7 +210,12 @@ class ConnectionTokenMixin:
class ConnectionTokenViewSet(ConnectionTokenMixin, RootOrgViewMixin, JMSModelViewSet):
filterset_fields = (
+<<<<<<< HEAD
'type', 'user_display', 'asset_display'
+=======
+ 'type', 'user_display', 'system_user_display',
+ 'application_display', 'asset_display'
+>>>>>>> origin
)
search_fields = filterset_fields
serializer_classes = {
@@ -215,7 +232,20 @@ class ConnectionTokenViewSet(ConnectionTokenMixin, RootOrgViewMixin, JMSModelVie
'get_rdp_file': 'authentication.add_connectiontoken',
'get_client_protocol_url': 'authentication.add_connectiontoken',
}
- queryset = ConnectionToken.objects.all()
+
+ def get_queryset(self):
+ return ConnectionToken.objects.filter(user=self.request.user)
+
+ def get_request_resource_user(self, serializer):
+ return self.request.user
+
+ def get_object(self):
+ if self.request.user.is_service_account:
+ # TODO: 组件获取 token 详情,将来放在 Super-connection-token API 中
+ obj = get_object_or_404(ConnectionToken, pk=self.kwargs.get('pk'))
+ else:
+ obj = super(ConnectionTokenViewSet, self).get_object()
+ return obj
def create_connection_token(self):
data = self.request.query_params if self.request.method == 'GET' else self.request.data
@@ -284,6 +314,9 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
'renewal': 'authentication.add_superconnectiontoken'
}
+ def get_request_resource_user(self, serializer):
+ return serializer.validated_data.get('user')
+
@action(methods=['PATCH'], detail=False)
def renewal(self, request, *args, **kwargs):
from common.utils.timezone import as_current_tz
@@ -299,4 +332,3 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
'msg': f'Token is renewed, date expired: {date_expired}'
}
return Response(data=data, status=status.HTTP_200_OK)
-
diff --git a/apps/authentication/api/login_confirm.py b/apps/authentication/api/login_confirm.py
index 22594b88e..866964677 100644
--- a/apps/authentication/api/login_confirm.py
+++ b/apps/authentication/api/login_confirm.py
@@ -6,6 +6,8 @@ from rest_framework.permissions import AllowAny
from common.utils import get_logger
from .. import errors, mixins
+from django.contrib.auth import logout as auth_logout
+
__all__ = ['TicketStatusApi']
logger = get_logger(__name__)
@@ -17,7 +19,15 @@ class TicketStatusApi(mixins.AuthMixin, APIView):
def get(self, request, *args, **kwargs):
try:
self.check_user_login_confirm()
+ self.request.session['auth_third_party_done'] = 1
return Response({"msg": "ok"})
+ except errors.LoginConfirmOtherError as e:
+ reason = e.msg
+ username = e.username
+ self.send_auth_signal(success=False, username=username, reason=reason)
+ # 若为三方登录,此时应退出登录
+ auth_logout(request)
+ return Response(e.as_data(), status=200)
except errors.NeedMoreInfoError as e:
return Response(e.as_data(), status=200)
diff --git a/apps/authentication/backends/base.py b/apps/authentication/backends/base.py
index 84cdeab27..13e99f2c6 100644
--- a/apps/authentication/backends/base.py
+++ b/apps/authentication/backends/base.py
@@ -49,7 +49,7 @@ class JMSBaseAuthBackend:
if not allow:
info = 'User {} skip authentication backend {}, because it not in {}'
info = info.format(username, backend_name, ','.join(allowed_backend_names))
- logger.debug(info)
+ logger.info(info)
return allow
diff --git a/apps/authentication/backends/cas/urls.py b/apps/authentication/backends/cas/urls.py
index 39a838b6a..376ce2332 100644
--- a/apps/authentication/backends/cas/urls.py
+++ b/apps/authentication/backends/cas/urls.py
@@ -3,9 +3,10 @@
from django.urls import path
import django_cas_ng.views
+from .views import CASLoginView
urlpatterns = [
- path('login/', django_cas_ng.views.LoginView.as_view(), name='cas-login'),
+ path('login/', CASLoginView.as_view(), name='cas-login'),
path('logout/', django_cas_ng.views.LogoutView.as_view(), name='cas-logout'),
path('callback/', django_cas_ng.views.CallbackView.as_view(), name='cas-proxy-callback'),
]
diff --git a/apps/authentication/backends/cas/views.py b/apps/authentication/backends/cas/views.py
new file mode 100644
index 000000000..f74e46e9c
--- /dev/null
+++ b/apps/authentication/backends/cas/views.py
@@ -0,0 +1,15 @@
+from django_cas_ng.views import LoginView
+from django.core.exceptions import PermissionDenied
+from django.http import HttpResponseRedirect
+
+__all__ = ['LoginView']
+
+
+class CASLoginView(LoginView):
+ def get(self, request):
+ try:
+ return super().get(request)
+ except PermissionDenied:
+ return HttpResponseRedirect('/')
+
+
diff --git a/apps/authentication/backends/oauth2/__init__.py b/apps/authentication/backends/oauth2/__init__.py
new file mode 100644
index 000000000..448096520
--- /dev/null
+++ b/apps/authentication/backends/oauth2/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+#
+
+from .backends import *
diff --git a/apps/authentication/backends/oauth2/backends.py b/apps/authentication/backends/oauth2/backends.py
new file mode 100644
index 000000000..755d5ef54
--- /dev/null
+++ b/apps/authentication/backends/oauth2/backends.py
@@ -0,0 +1,157 @@
+# -*- coding: utf-8 -*-
+#
+import requests
+
+from django.contrib.auth import get_user_model
+from django.utils.http import urlencode
+from django.conf import settings
+from django.urls import reverse
+
+from common.utils import get_logger
+from users.utils import construct_user_email
+from authentication.utils import build_absolute_uri
+from common.exceptions import JMSException
+
+from .signals import (
+ oauth2_create_or_update_user, oauth2_user_login_failed,
+ oauth2_user_login_success
+)
+from ..base import JMSModelBackend
+
+
+__all__ = ['OAuth2Backend']
+
+logger = get_logger(__name__)
+
+
+class OAuth2Backend(JMSModelBackend):
+ @staticmethod
+ def is_enabled():
+ return settings.AUTH_OAUTH2
+
+ def get_or_create_user_from_userinfo(self, request, userinfo):
+ log_prompt = "Get or Create user [OAuth2Backend]: {}"
+ logger.debug(log_prompt.format('start'))
+
+ # Construct user attrs value
+ user_attrs = {}
+ for field, attr in settings.AUTH_OAUTH2_USER_ATTR_MAP.items():
+ user_attrs[field] = userinfo.get(attr, '')
+
+ username = user_attrs.get('username')
+ if not username:
+ error_msg = 'username is missing'
+ logger.error(log_prompt.format(error_msg))
+ raise JMSException(error_msg)
+
+ email = user_attrs.get('email', '')
+ email = construct_user_email(user_attrs.get('username'), email)
+ user_attrs.update({'email': email})
+
+ logger.debug(log_prompt.format(user_attrs))
+ user, created = get_user_model().objects.get_or_create(
+ username=username, defaults=user_attrs
+ )
+ logger.debug(log_prompt.format("user: {}|created: {}".format(user, created)))
+ logger.debug(log_prompt.format("Send signal => oauth2 create or update user"))
+ oauth2_create_or_update_user.send(
+ sender=self.__class__, request=request, user=user, created=created,
+ attrs=user_attrs
+ )
+ return user, created
+
+ @staticmethod
+ def get_response_data(response_data):
+ if response_data.get('data') is not None:
+ response_data = response_data['data']
+ return response_data
+
+ @staticmethod
+ def get_query_dict(response_data, query_dict):
+ query_dict.update({
+ 'uid': response_data.get('uid', ''),
+ 'access_token': response_data.get('access_token', '')
+ })
+ return query_dict
+
+ def authenticate(self, request, code=None, **kwargs):
+ log_prompt = "Process authenticate [OAuth2Backend]: {}"
+ logger.debug(log_prompt.format('Start'))
+ if code is None:
+ logger.error(log_prompt.format('code is missing'))
+ return None
+
+ query_dict = {
+ 'client_id': settings.AUTH_OAUTH2_CLIENT_ID,
+ 'client_secret': settings.AUTH_OAUTH2_CLIENT_SECRET,
+ 'grant_type': 'authorization_code',
+ 'code': code,
+ 'redirect_uri': build_absolute_uri(
+ request, path=reverse(settings.AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME)
+ )
+ }
+ access_token_url = '{url}?{query}'.format(
+ url=settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT, query=urlencode(query_dict)
+ )
+ token_method = settings.AUTH_OAUTH2_ACCESS_TOKEN_METHOD.lower()
+ requests_func = getattr(requests, token_method, requests.get)
+ logger.debug(log_prompt.format('Call the access token endpoint[method: %s]' % token_method))
+ headers = {
+ 'Accept': 'application/json'
+ }
+ access_token_response = requests_func(access_token_url, headers=headers)
+ try:
+ access_token_response.raise_for_status()
+ access_token_response_data = access_token_response.json()
+ response_data = self.get_response_data(access_token_response_data)
+ except Exception as e:
+ error = "Json access token response error, access token response " \
+ "content is: {}, error is: {}".format(access_token_response.content, str(e))
+ logger.error(log_prompt.format(error))
+ return None
+
+ query_dict = self.get_query_dict(response_data, query_dict)
+
+ headers = {
+ 'Accept': 'application/json',
+ 'Authorization': 'token {}'.format(response_data.get('access_token', ''))
+ }
+
+ logger.debug(log_prompt.format('Get userinfo endpoint'))
+ userinfo_url = '{url}?{query}'.format(
+ url=settings.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT,
+ query=urlencode(query_dict)
+ )
+ userinfo_response = requests.get(userinfo_url, headers=headers)
+ try:
+ userinfo_response.raise_for_status()
+ userinfo_response_data = userinfo_response.json()
+ if 'data' in userinfo_response_data:
+ userinfo = userinfo_response_data['data']
+ else:
+ userinfo = userinfo_response_data
+ except Exception as e:
+ error = "Json userinfo response error, userinfo response " \
+ "content is: {}, error is: {}".format(userinfo_response.content, str(e))
+ logger.error(log_prompt.format(error))
+ return None
+
+ try:
+ logger.debug(log_prompt.format('Update or create oauth2 user'))
+ user, created = self.get_or_create_user_from_userinfo(request, userinfo)
+ except JMSException:
+ return None
+
+ if self.user_can_authenticate(user):
+ logger.debug(log_prompt.format('OAuth2 user login success'))
+ logger.debug(log_prompt.format('Send signal => oauth2 user login success'))
+ oauth2_user_login_success.send(sender=self.__class__, request=request, user=user)
+ return user
+ else:
+ logger.debug(log_prompt.format('OAuth2 user login failed'))
+ logger.debug(log_prompt.format('Send signal => oauth2 user login failed'))
+ oauth2_user_login_failed.send(
+ sender=self.__class__, request=request, username=user.username,
+ reason=_('User invalid, disabled or expired')
+ )
+ return None
diff --git a/apps/authentication/backends/oauth2/signals.py b/apps/authentication/backends/oauth2/signals.py
new file mode 100644
index 000000000..50c7837f8
--- /dev/null
+++ b/apps/authentication/backends/oauth2/signals.py
@@ -0,0 +1,9 @@
+from django.dispatch import Signal
+
+
+oauth2_create_or_update_user = Signal(
+ providing_args=['request', 'user', 'created', 'name', 'username', 'email']
+)
+oauth2_user_login_success = Signal(providing_args=['request', 'user'])
+oauth2_user_login_failed = Signal(providing_args=['request', 'username', 'reason'])
+
diff --git a/apps/authentication/backends/oauth2/urls.py b/apps/authentication/backends/oauth2/urls.py
new file mode 100644
index 000000000..94c044a7d
--- /dev/null
+++ b/apps/authentication/backends/oauth2/urls.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+#
+from django.urls import path
+
+from . import views
+
+
+urlpatterns = [
+ path('login/', views.OAuth2AuthRequestView.as_view(), name='login'),
+ path('callback/', views.OAuth2AuthCallbackView.as_view(), name='login-callback')
+]
diff --git a/apps/authentication/backends/oauth2/views.py b/apps/authentication/backends/oauth2/views.py
new file mode 100644
index 000000000..dd295fe86
--- /dev/null
+++ b/apps/authentication/backends/oauth2/views.py
@@ -0,0 +1,58 @@
+from django.views import View
+from django.conf import settings
+from django.contrib.auth import login
+from django.http import HttpResponseRedirect
+from django.urls import reverse
+from django.utils.http import urlencode
+
+from authentication.utils import build_absolute_uri
+from common.utils import get_logger
+from authentication.mixins import authenticate
+
+logger = get_logger(__file__)
+
+
+class OAuth2AuthRequestView(View):
+
+ def get(self, request):
+ log_prompt = "Process OAuth2 GET requests: {}"
+ logger.debug(log_prompt.format('Start'))
+
+ query_dict = {
+ 'client_id': settings.AUTH_OAUTH2_CLIENT_ID, 'response_type': 'code',
+ 'scope': settings.AUTH_OAUTH2_SCOPE,
+ 'redirect_uri': build_absolute_uri(
+ request, path=reverse(settings.AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME)
+ )
+ }
+
+ redirect_url = '{url}?{query}'.format(
+ url=settings.AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT,
+ query=urlencode(query_dict)
+ )
+ logger.debug(log_prompt.format('Redirect login url'))
+ return HttpResponseRedirect(redirect_url)
+
+
+class OAuth2AuthCallbackView(View):
+ http_method_names = ['get', ]
+
+ def get(self, request):
+ """ Processes GET requests. """
+ log_prompt = "Process GET requests [OAuth2AuthCallbackView]: {}"
+ logger.debug(log_prompt.format('Start'))
+ callback_params = request.GET
+
+ if 'code' in callback_params:
+ logger.debug(log_prompt.format('Process authenticate'))
+ user = authenticate(code=callback_params['code'], request=request)
+ if user and user.is_valid:
+ logger.debug(log_prompt.format('Login: {}'.format(user)))
+ login(self.request, user)
+ logger.debug(log_prompt.format('Redirect'))
+ return HttpResponseRedirect(
+ settings.AUTH_OAUTH2_AUTHENTICATION_REDIRECT_URI
+ )
+
+ logger.debug(log_prompt.format('Redirect'))
+ return HttpResponseRedirect(settings.AUTH_OAUTH2_AUTHENTICATION_FAILURE_REDIRECT_URI)
diff --git a/apps/authentication/backends/oidc/backends.py b/apps/authentication/backends/oidc/backends.py
index 9866e84f3..03b71334f 100644
--- a/apps/authentication/backends/oidc/backends.py
+++ b/apps/authentication/backends/oidc/backends.py
@@ -9,6 +9,7 @@
import base64
import requests
+
from rest_framework.exceptions import ParseError
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
@@ -18,10 +19,11 @@ from django.urls import reverse
from django.conf import settings
from common.utils import get_logger
+from authentication.utils import build_absolute_uri_for_oidc
from users.utils import construct_user_email
from ..base import JMSBaseAuthBackend
-from .utils import validate_and_return_id_token, build_absolute_uri
+from .utils import validate_and_return_id_token
from .decorator import ssl_verification
from .signals import (
openid_create_or_update_user, openid_user_login_failed, openid_user_login_success
@@ -127,7 +129,7 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
token_payload = {
'grant_type': 'authorization_code',
'code': code,
- 'redirect_uri': build_absolute_uri(
+ 'redirect_uri': build_absolute_uri_for_oidc(
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
)
}
diff --git a/apps/authentication/backends/oidc/utils.py b/apps/authentication/backends/oidc/utils.py
index 2a0f0609e..31cca6f03 100644
--- a/apps/authentication/backends/oidc/utils.py
+++ b/apps/authentication/backends/oidc/utils.py
@@ -8,7 +8,7 @@
import datetime as dt
from calendar import timegm
-from urllib.parse import urlparse, urljoin
+from urllib.parse import urlparse
from django.core.exceptions import SuspiciousOperation
from django.utils.encoding import force_bytes, smart_bytes
@@ -110,17 +110,3 @@ def _validate_claims(id_token, nonce=None, validate_nonce=True):
raise SuspiciousOperation('Incorrect id_token: nonce')
logger.debug(log_prompt.format('End'))
-
-
-def build_absolute_uri(request, path=None):
- """
- Build absolute redirect uri
- """
- if path is None:
- path = '/'
-
- if settings.BASE_SITE_URL:
- redirect_uri = urljoin(settings.BASE_SITE_URL, path)
- else:
- redirect_uri = request.build_absolute_uri(path)
- return redirect_uri
diff --git a/apps/authentication/backends/oidc/views.py b/apps/authentication/backends/oidc/views.py
index 1c9442ef2..78019ac33 100644
--- a/apps/authentication/backends/oidc/views.py
+++ b/apps/authentication/backends/oidc/views.py
@@ -20,7 +20,8 @@ from django.utils.crypto import get_random_string
from django.utils.http import is_safe_url, urlencode
from django.views.generic import View
-from .utils import get_logger, build_absolute_uri
+from authentication.utils import build_absolute_uri_for_oidc
+from .utils import get_logger
logger = get_logger(__file__)
@@ -50,7 +51,7 @@ class OIDCAuthRequestView(View):
'scope': settings.AUTH_OPENID_SCOPES,
'response_type': 'code',
'client_id': settings.AUTH_OPENID_CLIENT_ID,
- 'redirect_uri': build_absolute_uri(
+ 'redirect_uri': build_absolute_uri_for_oidc(
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
)
})
@@ -216,7 +217,7 @@ class OIDCEndSessionView(View):
""" Returns the end-session URL. """
q = QueryDict(mutable=True)
q[settings.AUTH_OPENID_PROVIDER_END_SESSION_REDIRECT_URI_PARAMETER] = \
- build_absolute_uri(self.request, path=settings.LOGOUT_REDIRECT_URL or '/')
+ build_absolute_uri_for_oidc(self.request, path=settings.LOGOUT_REDIRECT_URL or '/')
q[settings.AUTH_OPENID_PROVIDER_END_SESSION_ID_TOKEN_PARAMETER] = \
self.request.session['oidc_auth_id_token']
return '{}?{}'.format(settings.AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT, q.urlencode())
diff --git a/apps/authentication/backends/saml2/backends.py b/apps/authentication/backends/saml2/backends.py
index 0ac0efe1c..70667cd94 100644
--- a/apps/authentication/backends/saml2/backends.py
+++ b/apps/authentication/backends/saml2/backends.py
@@ -39,7 +39,7 @@ class SAML2Backend(JMSModelBackend):
return user, created
def authenticate(self, request, saml_user_data=None, **kwargs):
- log_prompt = "Process authenticate [SAML2AuthCodeBackend]: {}"
+ log_prompt = "Process authenticate [SAML2Backend]: {}"
logger.debug(log_prompt.format('Start'))
if saml_user_data is None:
logger.error(log_prompt.format('saml_user_data is missing'))
@@ -48,7 +48,7 @@ class SAML2Backend(JMSModelBackend):
logger.debug(log_prompt.format('saml data, {}'.format(saml_user_data)))
username = saml_user_data.get('username')
if not username:
- logger.debug(log_prompt.format('username is missing'))
+ logger.warning(log_prompt.format('username is missing'))
return None
user, created = self.get_or_create_from_saml_data(request, **saml_user_data)
diff --git a/apps/authentication/errors/failed.py b/apps/authentication/errors/failed.py
index 118fd6d6e..c85695fe5 100644
--- a/apps/authentication/errors/failed.py
+++ b/apps/authentication/errors/failed.py
@@ -12,12 +12,13 @@ class AuthFailedNeedLogMixin:
username = ''
request = None
error = ''
+ msg = ''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
post_auth_failed.send(
sender=self.__class__, username=self.username,
- request=self.request, reason=self.error
+ request=self.request, reason=self.msg
)
@@ -55,7 +56,8 @@ class BlockGlobalIpLoginError(AuthFailedError):
error = 'block_global_ip_login'
def __init__(self, username, ip, **kwargs):
- self.msg = const.block_ip_login_msg.format(settings.SECURITY_LOGIN_IP_LIMIT_TIME)
+ if not self.msg:
+ self.msg = const.block_ip_login_msg.format(settings.SECURITY_LOGIN_IP_LIMIT_TIME)
LoginIpBlockUtil(ip).set_block_if_need()
super().__init__(username=username, ip=ip, **kwargs)
@@ -65,22 +67,21 @@ class CredentialError(
BlockGlobalIpLoginError, AuthFailedError
):
def __init__(self, error, username, ip, request):
- super().__init__(error=error, username=username, ip=ip, request=request)
util = LoginBlockUtil(username, ip)
times_remainder = util.get_remainder_times()
block_time = settings.SECURITY_LOGIN_LIMIT_TIME
-
if times_remainder < 1:
self.msg = const.block_user_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
- return
-
- default_msg = const.invalid_login_msg.format(
- times_try=times_remainder, block_time=block_time
- )
- if error == const.reason_password_failed:
- self.msg = default_msg
else:
- self.msg = const.reason_choices.get(error, default_msg)
+ default_msg = const.invalid_login_msg.format(
+ times_try=times_remainder, block_time=block_time
+ )
+ if error == const.reason_password_failed:
+ self.msg = default_msg
+ else:
+ self.msg = const.reason_choices.get(error, default_msg)
+ # 先处理 msg 在 super,记录日志时原因才准确
+ super().__init__(error=error, username=username, ip=ip, request=request)
class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError):
@@ -138,18 +139,11 @@ class ACLError(AuthFailedNeedLogMixin, AuthFailedError):
}
-class LoginIPNotAllowed(ACLError):
+class LoginACLIPAndTimePeriodNotAllowed(ACLError):
def __init__(self, username, request, **kwargs):
self.username = username
self.request = request
- super().__init__(_("IP is not allowed"), **kwargs)
-
-
-class TimePeriodNotAllowed(ACLError):
- def __init__(self, username, request, **kwargs):
- self.username = username
- self.request = request
- super().__init__(_("Time Period is not allowed"), **kwargs)
+ super().__init__(_("Current IP and Time period is not allowed"), **kwargs)
class MFACodeRequiredError(AuthFailedError):
diff --git a/apps/authentication/errors/mfa.py b/apps/authentication/errors/mfa.py
index 4b7f5a57e..288c2060e 100644
--- a/apps/authentication/errors/mfa.py
+++ b/apps/authentication/errors/mfa.py
@@ -14,23 +14,23 @@ class WeComCodeInvalid(JMSException):
class WeComBindAlready(JMSException):
- default_code = 'wecom_bind_already'
- default_detail = 'WeCom already binded'
+ default_code = 'wecom_not_bound'
+ default_detail = _('WeCom is already bound')
class WeComNotBound(JMSException):
default_code = 'wecom_not_bound'
- default_detail = 'WeCom is not bound'
+ default_detail = _('WeCom is not bound')
class DingTalkNotBound(JMSException):
default_code = 'dingtalk_not_bound'
- default_detail = 'DingTalk is not bound'
+ default_detail = _('DingTalk is not bound')
class FeiShuNotBound(JMSException):
default_code = 'feishu_not_bound'
- default_detail = 'FeiShu is not bound'
+ default_detail = _('FeiShu is not bound')
class PasswordInvalid(JMSException):
diff --git a/apps/authentication/errors/redirect.py b/apps/authentication/errors/redirect.py
index bf334133d..466ec708d 100644
--- a/apps/authentication/errors/redirect.py
+++ b/apps/authentication/errors/redirect.py
@@ -69,10 +69,16 @@ class LoginConfirmWaitError(LoginConfirmBaseError):
class LoginConfirmOtherError(LoginConfirmBaseError):
error = 'login_confirm_error'
- def __init__(self, ticket_id, status):
+ def __init__(self, ticket_id, status, username):
+ self.username = username
msg = const.login_confirm_error_msg.format(status)
super().__init__(ticket_id=ticket_id, msg=msg)
+ def as_data(self):
+ ret = super().as_data()
+ ret['data']['username'] = self.username
+ return ret
+
class PasswordTooSimple(NeedRedirectError):
default_code = 'passwd_too_simple'
diff --git a/apps/authentication/middleware.py b/apps/authentication/middleware.py
index b4241f12d..5b6d7c06f 100644
--- a/apps/authentication/middleware.py
+++ b/apps/authentication/middleware.py
@@ -1,11 +1,16 @@
import base64
-from django.shortcuts import redirect, reverse
+from django.shortcuts import redirect, reverse, render
from django.utils.deprecation import MiddlewareMixin
from django.http import HttpResponse
from django.conf import settings
+from django.utils.translation import ugettext as _
+from django.contrib.auth import logout as auth_logout
+from apps.authentication import mixins
from common.utils import gen_key_pair
+from common.utils import get_request_ip
+from .signals import post_auth_failed
class MFAMiddleware:
@@ -13,6 +18,7 @@ class MFAMiddleware:
这个 中间件 是用来全局拦截开启了 MFA 却没有认证的,如 OIDC, CAS,使用第三方库做的登录,直接 login 了,
所以只能在 Middleware 中控制
"""
+
def __init__(self, get_response):
self.get_response = get_response
@@ -42,6 +48,50 @@ class MFAMiddleware:
return redirect(url)
+class ThirdPartyLoginMiddleware(mixins.AuthMixin):
+ """OpenID、CAS、SAML2登录规则设置验证"""
+
+ def __init__(self, get_response):
+ self.get_response = get_response
+
+ def __call__(self, request):
+ response = self.get_response(request)
+ # 没有认证过,证明不是从 第三方 来的
+ if request.user.is_anonymous:
+ return response
+ if not request.session.get('auth_third_party_required'):
+ return response
+ ip = get_request_ip(request)
+ try:
+ self.request = request
+ self._check_login_acl(request.user, ip)
+ except Exception as e:
+ post_auth_failed.send(
+ sender=self.__class__, username=request.user.username,
+ request=self.request, reason=e.msg
+ )
+ auth_logout(request)
+ context = {
+ 'title': _('Authentication failed'),
+ 'message': _('Authentication failed (before login check failed): {}').format(e),
+ 'interval': 10,
+ 'redirect_url': reverse('authentication:login'),
+ 'auto_redirect': True,
+ }
+ response = render(request, 'authentication/auth_fail_flash_message_standalone.html', context)
+ else:
+ if not self.request.session['auth_confirm_required']:
+ return response
+ guard_url = reverse('authentication:login-guard')
+ args = request.META.get('QUERY_STRING', '')
+ if args:
+ guard_url = "%s?%s" % (guard_url, args)
+ response = redirect(guard_url)
+ finally:
+ request.session.pop('auth_third_party_required', '')
+ return response
+
+
class SessionCookieMiddleware(MiddlewareMixin):
@staticmethod
diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py
index 403f7186d..7341b4bd1 100644
--- a/apps/authentication/mixins.py
+++ b/apps/authentication/mixins.py
@@ -328,13 +328,59 @@ class AuthACLMixin:
def _check_login_acl(self, user, ip):
# ACL 限制用户登录
- is_allowed, limit_type = LoginACL.allow_user_to_login(user, ip)
- if is_allowed:
+ acl = LoginACL.match(user, ip)
+ if not acl:
return
- if limit_type == 'ip':
- raise errors.LoginIPNotAllowed(username=user.username, request=self.request)
- elif limit_type == 'time':
- raise errors.TimePeriodNotAllowed(username=user.username, request=self.request)
+
+ acl: LoginACL
+ if acl.is_action(acl.ActionChoices.allow):
+ return
+
+ if acl.is_action(acl.ActionChoices.reject):
+ raise errors.LoginACLIPAndTimePeriodNotAllowed(user.username, request=self.request)
+
+ if acl.is_action(acl.ActionChoices.confirm):
+ self.request.session['auth_confirm_required'] = '1'
+ self.request.session['auth_acl_id'] = str(acl.id)
+ return
+
+ def check_user_login_confirm_if_need(self, user):
+ if not self.request.session.get("auth_confirm_required"):
+ return
+ acl_id = self.request.session.get('auth_acl_id')
+ logger.debug('Login confirm acl id: {}'.format(acl_id))
+ if not acl_id:
+ return
+ acl = LoginACL.filter_acl(user).filter(id=acl_id).first()
+ if not acl:
+ return
+ if not acl.is_action(acl.ActionChoices.confirm):
+ return
+ self.get_ticket_or_create(acl)
+ self.check_user_login_confirm()
+
+ def get_ticket_or_create(self, acl):
+ ticket = self.get_ticket()
+ if not ticket or ticket.is_state(ticket.State.closed):
+ ticket = acl.create_confirm_ticket(self.request)
+ self.request.session['auth_ticket_id'] = str(ticket.id)
+ return ticket
+
+ def check_user_login_confirm(self):
+ ticket = self.get_ticket()
+ if not ticket:
+ raise errors.LoginConfirmOtherError('', "Not found")
+ elif ticket.is_state(ticket.State.approved):
+ self.request.session["auth_confirm_required"] = ''
+ return
+ elif ticket.is_status(ticket.Status.open):
+ raise errors.LoginConfirmWaitError(ticket.id)
+ else:
+ # rejected, closed
+ ticket_id = ticket.id
+ status = ticket.get_state_display()
+ username = ticket.applicant.username
+ raise errors.LoginConfirmOtherError(ticket_id, status, username)
def get_ticket(self):
from tickets.models import ApplyLoginTicket
@@ -346,44 +392,6 @@ class AuthACLMixin:
ticket = ApplyLoginTicket.all().filter(id=ticket_id).first()
return ticket
- def get_ticket_or_create(self, confirm_setting):
- ticket = self.get_ticket()
- if not ticket or ticket.is_status(ticket.Status.closed):
- ticket = confirm_setting.create_confirm_ticket(self.request)
- self.request.session['auth_ticket_id'] = str(ticket.id)
- return ticket
-
- def check_user_login_confirm(self):
- ticket = self.get_ticket()
- if not ticket:
- raise errors.LoginConfirmOtherError('', "Not found")
-
- if ticket.is_status(ticket.Status.open):
- raise errors.LoginConfirmWaitError(ticket.id)
- elif ticket.is_state(ticket.State.approved):
- self.request.session["auth_confirm"] = "1"
- return
- elif ticket.is_state(ticket.State.rejected):
- raise errors.LoginConfirmOtherError(
- ticket.id, ticket.get_state_display()
- )
- elif ticket.is_state(ticket.State.closed):
- raise errors.LoginConfirmOtherError(
- ticket.id, ticket.get_state_display()
- )
- else:
- raise errors.LoginConfirmOtherError(
- ticket.id, ticket.get_status_display()
- )
-
- def check_user_login_confirm_if_need(self, user):
- ip = self.get_request_ip()
- is_allowed, confirm_setting = LoginACL.allow_user_confirm_if_need(user, ip)
- if self.request.session.get('auth_confirm') or not is_allowed:
- return
- self.get_ticket_or_create(confirm_setting)
- self.check_user_login_confirm()
-
class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPostCheckMixin):
request = None
@@ -482,7 +490,9 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost
return self.check_user_auth(valid_data)
def clear_auth_mark(self):
- keys = ['auth_password', 'user_id', 'auth_confirm', 'auth_ticket_id']
+ keys = [
+ 'auth_password', 'user_id', 'auth_confirm_required', 'auth_ticket_id', 'auth_acl_id'
+ ]
for k in keys:
self.request.session.pop(k, '')
diff --git a/apps/authentication/models.py b/apps/authentication/models.py
index eebaa22a6..ea4dbb5ff 100644
--- a/apps/authentication/models.py
+++ b/apps/authentication/models.py
@@ -216,6 +216,13 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
return {}
return self.application.get_rdp_remote_app_setting()
+ @lazyproperty
+ def asset_or_remote_app_asset(self):
+ if self.asset:
+ return self.asset
+ if self.application and self.application.category_remote_app:
+ return self.application.get_remote_app_asset()
+
@lazyproperty
def cmd_filter_rules(self):
from assets.models import CommandFilterRule
diff --git a/apps/authentication/serializers/connection_token.py b/apps/authentication/serializers/connection_token.py
index acbce801e..5a3db5b4e 100644
--- a/apps/authentication/serializers/connection_token.py
+++ b/apps/authentication/serializers/connection_token.py
@@ -25,9 +25,8 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
model = ConnectionToken
fields_mini = ['id', 'type']
fields_small = fields_mini + [
- 'secret', 'date_expired',
- 'date_created', 'date_updated', 'created_by', 'updated_by',
- 'org_id', 'org_name',
+ 'secret', 'date_expired', 'date_created', 'date_updated',
+ 'created_by', 'updated_by', 'org_id', 'org_name',
]
fields_fk = [
'user', 'system_user', 'asset', 'application',
@@ -35,8 +34,8 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
read_only_fields = [
# 普通 Token 不支持指定 user
'user', 'is_valid', 'expire_time',
- 'type_display', 'user_display', 'system_user_display', 'asset_display',
- 'application_display',
+ 'type_display', 'user_display', 'system_user_display',
+ 'asset_display', 'application_display',
]
fields = fields_small + fields_fk + read_only_fields
@@ -59,7 +58,7 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
system_user = attrs.get('system_user') or ''
asset = attrs.get('asset') or ''
application = attrs.get('application') or ''
- secret = attrs.get('secret') or random_string(64)
+ secret = attrs.get('secret') or random_string(16)
date_expired = attrs.get('date_expired') or ConnectionToken.get_default_date_expired()
if isinstance(asset, Asset):
@@ -97,8 +96,8 @@ class SuperConnectionTokenSerializer(ConnectionTokenSerializer):
class Meta(ConnectionTokenSerializer.Meta):
read_only_fields = [
- 'validity',
- 'user_display', 'system_user_display', 'asset_display', 'application_display',
+ 'validity', 'user_display', 'system_user_display',
+ 'asset_display', 'application_display',
]
def get_user(self, attrs):
@@ -154,7 +153,12 @@ class ConnectionTokenCmdFilterRuleSerializer(serializers.ModelSerializer):
class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
user = ConnectionTokenUserSerializer(read_only=True)
+<<<<<<< HEAD
asset = ConnectionTokenAssetSerializer(read_only=True)
+=======
+ asset = ConnectionTokenAssetSerializer(read_only=True, source='asset_or_remote_app_asset')
+ application = ConnectionTokenApplicationSerializer(read_only=True)
+>>>>>>> origin
remote_app = ConnectionTokenRemoteAppSerializer(read_only=True)
account = serializers.CharField(read_only=True)
gateway = ConnectionTokenGatewaySerializer(read_only=True)
diff --git a/apps/authentication/signal_handlers.py b/apps/authentication/signal_handlers.py
index ac155dcf0..e7be3465c 100644
--- a/apps/authentication/signal_handlers.py
+++ b/apps/authentication/signal_handlers.py
@@ -6,12 +6,16 @@ from django.core.cache import cache
from django.dispatch import receiver
from django_cas_ng.signals import cas_user_authenticated
+from apps.jumpserver.settings.auth import AUTHENTICATION_BACKENDS_THIRD_PARTY
from authentication.backends.oidc.signals import (
openid_user_login_failed, openid_user_login_success
)
from authentication.backends.saml2.signals import (
saml2_user_authenticated, saml2_user_authentication_failed
)
+from authentication.backends.oauth2.signals import (
+ oauth2_user_login_failed, oauth2_user_login_success
+)
from .signals import post_auth_success, post_auth_failed
@@ -25,7 +29,8 @@ def on_user_auth_login_success(sender, user, request, **kwargs):
and user.mfa_enabled \
and not request.session.get('auth_mfa'):
request.session['auth_mfa_required'] = 1
-
+ if not request.session.get("auth_third_party_done") and request.session.get('auth_backend') in AUTHENTICATION_BACKENDS_THIRD_PARTY:
+ request.session['auth_third_party_required'] = 1
# 单点登录,超过了自动退出
if settings.USER_LOGIN_SINGLE_MACHINE_ENABLED:
lock_key = 'single_machine_login_' + str(user.id)
@@ -67,3 +72,15 @@ def on_saml2_user_login_success(sender, request, user, **kwargs):
def on_saml2_user_login_failed(sender, request, username, reason, **kwargs):
request.session['auth_backend'] = settings.AUTH_BACKEND_SAML2
post_auth_failed.send(sender, username=username, request=request, reason=reason)
+
+
+@receiver(oauth2_user_login_success)
+def on_oauth2_user_login_success(sender, request, user, **kwargs):
+ request.session['auth_backend'] = settings.AUTH_BACKEND_OAUTH2
+ post_auth_success.send(sender, user=user, request=request)
+
+
+@receiver(oauth2_user_login_failed)
+def on_oauth2_user_login_failed(sender, username, request, reason, **kwargs):
+ request.session['auth_backend'] = settings.AUTH_BACKEND_OAUTH2
+ post_auth_failed.send(sender, username=username, request=request, reason=reason)
diff --git a/apps/authentication/templates/authentication/auth_fail_flash_message_standalone.html b/apps/authentication/templates/authentication/auth_fail_flash_message_standalone.html
new file mode 100644
index 000000000..61b431b9a
--- /dev/null
+++ b/apps/authentication/templates/authentication/auth_fail_flash_message_standalone.html
@@ -0,0 +1,70 @@
+{% extends '_base_only_content.html' %}
+{% load static %}
+{% load i18n %}
+{% block html_title %} {{ title }} {% endblock %}
+{% block title %} {{ title }}{% endblock %}
+
+{% block content %}
+
+
+
+
+ {% if error %}
+ {{ error }}
+ {% else %}
+ {{ message|safe }}
+ {% endif %}
+
+
+
+
+ {% if has_cancel %}
+
+ {% endif %}
+
+
+
+{% endblock %}
+
+{% block custom_foot_js %}
+
+{% endblock %}
+
diff --git a/apps/authentication/templates/authentication/login_wait_confirm.html b/apps/authentication/templates/authentication/login_wait_confirm.html
index bd94c1986..157ee1d1a 100644
--- a/apps/authentication/templates/authentication/login_wait_confirm.html
+++ b/apps/authentication/templates/authentication/login_wait_confirm.html
@@ -79,6 +79,9 @@ function doRequestAuth() {
requestApi({
url: url,
method: "GET",
+ headers: {
+ "X-JMS-LOGIN-TYPE": "W"
+ },
success: function (data) {
if (!data.error && data.msg === 'ok') {
window.onbeforeunload = function(){};
@@ -98,7 +101,7 @@ function doRequestAuth() {
},
error: function (text, data) {
},
- flash_message: false
+ flash_message: false, // 是否显示flash消息
})
}
function initClipboard() {
diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py
index 9abd61e3b..2c97fe1ea 100644
--- a/apps/authentication/urls/view_urls.py
+++ b/apps/authentication/urls/view_urls.py
@@ -56,9 +56,11 @@ urlpatterns = [
path('profile/otp/disable/', users_view.UserOtpDisableView.as_view(),
name='user-otp-disable'),
- # openid
+ # other authentication protocol
path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')),
path('openid/', include(('authentication.backends.oidc.urls', 'authentication'), namespace='openid')),
path('saml2/', include(('authentication.backends.saml2.urls', 'authentication'), namespace='saml2')),
+ path('oauth2/', include(('authentication.backends.oauth2.urls', 'authentication'), namespace='oauth2')),
+
path('captcha/', include('captcha.urls')),
]
diff --git a/apps/authentication/utils.py b/apps/authentication/utils.py
index c0588b206..a0db9061c 100644
--- a/apps/authentication/utils.py
+++ b/apps/authentication/utils.py
@@ -1,7 +1,10 @@
# -*- coding: utf-8 -*-
#
+import ipaddress
+from urllib.parse import urljoin, urlparse
from django.conf import settings
+from django.utils.translation import ugettext_lazy as _
from common.utils import validate_ip, get_ip_city, get_request_ip
from common.utils import get_logger
@@ -22,10 +25,34 @@ def check_different_city_login_if_need(user, request):
else:
city = get_ip_city(ip) or DEFAULT_CITY
- city_white = ['LAN', ]
- if city not in city_white:
+ city_white = [_('LAN'), 'LAN']
+ is_private = ipaddress.ip_address(ip).is_private
+ if not is_private:
last_user_login = UserLoginLog.objects.exclude(city__in=city_white) \
.filter(username=user.username, status=True).first()
if last_user_login and last_user_login.city != city:
DifferentCityLoginMessage(user, ip, city).publish_async()
+
+
+def build_absolute_uri(request, path=None):
+ """ Build absolute redirect """
+ if path is None:
+ path = '/'
+ site_url = urlparse(settings.SITE_URL)
+ scheme = site_url.scheme or request.scheme
+ host = request.get_host()
+ url = f'{scheme}://{host}'
+ redirect_uri = urljoin(url, path)
+ return redirect_uri
+
+
+def build_absolute_uri_for_oidc(request, path=None):
+ """ Build absolute redirect uri for OIDC """
+ if path is None:
+ path = '/'
+ if settings.BASE_SITE_URL:
+ # OIDC 专用配置项
+ redirect_uri = urljoin(settings.BASE_SITE_URL, path)
+ return redirect_uri
+ return build_absolute_uri(request, path=path)
diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py
index 09c842d88..88f580279 100644
--- a/apps/authentication/views/login.py
+++ b/apps/authentication/views/login.py
@@ -21,7 +21,7 @@ from django.conf import settings
from django.urls import reverse_lazy
from django.contrib.auth import BACKEND_SESSION_KEY
-from common.utils import FlashMessageUtil
+from common.utils import FlashMessageUtil, static_or_direct
from users.utils import (
redirect_user_first_login_or_index
)
@@ -39,8 +39,7 @@ class UserLoginContextMixin:
get_user_mfa_context: Callable
request: HttpRequest
- @staticmethod
- def get_support_auth_methods():
+ def get_support_auth_methods(self):
auth_methods = [
{
'name': 'OpenID',
@@ -63,6 +62,13 @@ class UserLoginContextMixin:
'logo': static('img/login_saml2_logo.png'),
'auto_redirect': True
},
+ {
+ 'name': settings.AUTH_OAUTH2_PROVIDER,
+ 'enabled': settings.AUTH_OAUTH2,
+ 'url': reverse('authentication:oauth2:login'),
+ 'logo': static_or_direct(settings.AUTH_OAUTH2_LOGO_PATH),
+ 'auto_redirect': True
+ },
{
'name': _('WeCom'),
'enabled': settings.AUTH_WECOM,
diff --git a/apps/common/drf/metadata.py b/apps/common/drf/metadata.py
index 7e4d32527..a3a74cf31 100644
--- a/apps/common/drf/metadata.py
+++ b/apps/common/drf/metadata.py
@@ -68,7 +68,7 @@ class SimpleMetadataWithFilters(SimpleMetadata):
default = getattr(field, 'default', None)
if default is not None and default != empty:
- if isinstance(default, (str, int, bool, datetime.datetime, list)):
+ if isinstance(default, (str, int, bool, float, datetime.datetime, list)):
field_info['default'] = default
for attr in self.attrs:
diff --git a/apps/common/hashers/__init__.py b/apps/common/hashers/__init__.py
new file mode 100644
index 000000000..6313fa88b
--- /dev/null
+++ b/apps/common/hashers/__init__.py
@@ -0,0 +1 @@
+from .sm3 import PBKDF2SM3PasswordHasher
diff --git a/apps/common/hashers/sm3.py b/apps/common/hashers/sm3.py
new file mode 100644
index 000000000..62f811e94
--- /dev/null
+++ b/apps/common/hashers/sm3.py
@@ -0,0 +1,23 @@
+from gmssl import sm3, func
+
+from django.contrib.auth.hashers import PBKDF2PasswordHasher
+
+
+class Hasher:
+ name = 'sm3'
+
+ def __init__(self, key):
+ self.key = key
+
+ def hexdigest(self):
+ return sm3.sm3_hash(func.bytes_to_list(self.key))
+
+ @staticmethod
+ def hash(msg):
+ return Hasher(msg)
+
+
+class PBKDF2SM3PasswordHasher(PBKDF2PasswordHasher):
+ algorithm = "pbkdf2_sm3"
+ digest = Hasher.hash
+
diff --git a/apps/common/permissions.py b/apps/common/permissions.py
index 3000b9533..869107a58 100644
--- a/apps/common/permissions.py
+++ b/apps/common/permissions.py
@@ -7,6 +7,9 @@ from rest_framework import permissions
from authentication.const import ConfirmType
from common.exceptions import UserConfirmRequired
+from orgs.utils import tmp_to_root_org
+from authentication.models import ConnectionToken
+from common.utils import get_object_or_none
class IsValidUser(permissions.IsAuthenticated, permissions.BasePermission):
@@ -17,6 +20,22 @@ class IsValidUser(permissions.IsAuthenticated, permissions.BasePermission):
and request.user.is_valid
+class IsValidUserOrConnectionToken(IsValidUser):
+
+ def has_permission(self, request, view):
+ return super(IsValidUserOrConnectionToken, self).has_permission(request, view) \
+ or self.is_valid_connection_token(request)
+
+ @staticmethod
+ def is_valid_connection_token(request):
+ token_id = request.query_params.get('token')
+ if not token_id:
+ return False
+ with tmp_to_root_org():
+ token = get_object_or_none(ConnectionToken, id=token_id)
+ return token and token.is_valid
+
+
class OnlySuperUser(IsValidUser):
def has_permission(self, request, view):
return super().has_permission(request, view) \
@@ -38,6 +57,9 @@ class UserConfirmation(permissions.BasePermission):
confirm_type = ConfirmType.ReLogin
def has_permission(self, request, view):
+ if not settings.SECURITY_VIEW_AUTH_NEED_MFA:
+ return True
+
confirm_level = request.session.get('CONFIRM_LEVEL')
confirm_time = request.session.get('CONFIRM_TIME')
diff --git a/apps/common/sdk/sms/base.py b/apps/common/sdk/sms/base.py
index 4d02370b1..77dcc669a 100644
--- a/apps/common/sdk/sms/base.py
+++ b/apps/common/sdk/sms/base.py
@@ -17,4 +17,8 @@ class BaseSMSClient:
def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
raise NotImplementedError
+ @staticmethod
+ def need_pre_check():
+ return True
+
diff --git a/apps/common/sdk/sms/cmpp2.py b/apps/common/sdk/sms/cmpp2.py
new file mode 100644
index 000000000..4d81ee1a0
--- /dev/null
+++ b/apps/common/sdk/sms/cmpp2.py
@@ -0,0 +1,329 @@
+import hashlib
+import socket
+import struct
+import time
+
+from django.conf import settings
+from django.utils.translation import ugettext_lazy as _
+
+from common.utils import get_logger
+from common.exceptions import JMSException
+from .base import BaseSMSClient
+
+
+logger = get_logger(__file__)
+
+
+CMPP_CONNECT = 0x00000001 # 请求连接
+CMPP_CONNECT_RESP = 0x80000001 # 请求连接应答
+CMPP_TERMINATE = 0x00000002 # 终止连接
+CMPP_TERMINATE_RESP = 0x80000002 # 终止连接应答
+CMPP_SUBMIT = 0x00000004 # 提交短信
+CMPP_SUBMIT_RESP = 0x80000004 # 提交短信应答
+CMPP_DELIVER = 0x00000005 # 短信下发
+CMPP_DELIVER_RESP = 0x80000005 # 下发短信应答
+
+
+class CMPPBaseRequestInstance(object):
+ def __init__(self):
+ self.command_id = ''
+ self.body = b''
+ self.length = 0
+
+ def get_header(self, sequence_id):
+ length = struct.pack('!L', 12 + self.length)
+ command_id = struct.pack('!L', self.command_id)
+ sequence_id = struct.pack('!L', sequence_id)
+ return length + command_id + sequence_id
+
+ def get_message(self, sequence_id):
+ return self.get_header(sequence_id) + self.body
+
+
+class CMPPConnectRequestInstance(CMPPBaseRequestInstance):
+ def __init__(self, sp_id, sp_secret):
+ if len(sp_id) != 6:
+ raise ValueError(_("sp_id is 6 bits"))
+
+ super().__init__()
+
+ source_addr = sp_id.encode('utf-8')
+ sp_secret = sp_secret.encode('utf-8')
+ version = struct.pack('!B', 0x02)
+ timestamp = struct.pack('!L', int(self.get_now()))
+ authenticator_source = source_addr + 9 * b'\x00' + sp_secret + self.get_now().encode('utf-8')
+ auth_source_md5 = hashlib.md5(authenticator_source).digest()
+ self.body = source_addr + auth_source_md5 + version + timestamp
+ self.length = len(self.body)
+ self.command_id = CMPP_CONNECT
+
+ @staticmethod
+ def get_now():
+ return time.strftime('%m%d%H%M%S', time.localtime(time.time()))
+
+
+class CMPPSubmitRequestInstance(CMPPBaseRequestInstance):
+ def __init__(self, msg_src, dest_terminal_id, msg_content, src_id,
+ service_id='', dest_usr_tl=1):
+ if len(msg_content) >= 70:
+ raise JMSException('The message length should be within 70 characters')
+ if len(dest_terminal_id) > 100:
+ raise JMSException('The number of users receiving information should be less than 100')
+
+ super().__init__()
+
+ msg_id = 8 * b'\x00'
+ pk_total = struct.pack('!B', 1)
+ pk_number = struct.pack('!B', 1)
+ registered_delivery = struct.pack('!B', 0)
+ msg_level = struct.pack('!B', 0)
+ service_id = ((10 - len(service_id)) * '\x00' + service_id).encode('utf-8')
+ fee_user_type = struct.pack('!B', 2)
+ fee_terminal_id = ('0' * 21).encode('utf-8')
+ tp_pid = struct.pack('!B', 0)
+ tp_udhi = struct.pack('!B', 0)
+ msg_fmt = struct.pack('!B', 8)
+ fee_type = '01'.encode('utf-8')
+ fee_code = '000000'.encode('utf-8')
+ valid_time = ('\x00' * 17).encode('utf-8')
+ at_time = ('\x00' * 17).encode('utf-8')
+ src_id = ((21 - len(src_id)) * '\x00' + src_id).encode('utf-8')
+ reserve = b'\x00' * 8
+ _msg_length = struct.pack('!B', len(msg_content) * 2)
+ _msg_src = msg_src.encode('utf-8')
+ _dest_usr_tl = struct.pack('!B', dest_usr_tl)
+ _msg_content = msg_content.encode('utf-16-be')
+ _dest_terminal_id = b''.join([
+ (i + (21 - len(i)) * '\x00').encode('utf-8') for i in dest_terminal_id
+ ])
+ self.length = 126 + 21 * dest_usr_tl + len(_msg_content)
+ self.command_id = CMPP_SUBMIT
+ self.body = msg_id + pk_total + pk_number + registered_delivery \
+ + msg_level + service_id + fee_user_type + fee_terminal_id \
+ + tp_pid + tp_udhi + msg_fmt + _msg_src + fee_type + fee_code \
+ + valid_time + at_time + src_id + _dest_usr_tl + _dest_terminal_id \
+ + _msg_length + _msg_content + reserve
+
+
+class CMPPTerminateRequestInstance(CMPPBaseRequestInstance):
+ def __init__(self):
+ super().__init__()
+ self.body = b''
+ self.command_id = CMPP_TERMINATE
+
+
+class CMPPDeliverRespRequestInstance(CMPPBaseRequestInstance):
+ def __init__(self, msg_id, result=0):
+ super().__init__()
+ msg_id = struct.pack('!Q', msg_id)
+ result = struct.pack('!B', result)
+ self.length = len(self.body)
+ self.body = msg_id + result
+
+
+class CMPPResponseInstance(object):
+ def __init__(self):
+ self.command_id = None
+ self.length = None
+ self.response_handler_map = {
+ CMPP_CONNECT_RESP: self.connect_response_parse,
+ CMPP_SUBMIT_RESP: self.submit_response_parse,
+ CMPP_DELIVER: self.deliver_request_parse,
+ }
+
+ @staticmethod
+ def connect_response_parse(body):
+ status, = struct.unpack('!B', body[0:1])
+ authenticator_ISMG = body[1:17]
+ version, = struct.unpack('!B', body[17:18])
+ return {
+ 'Status': status,
+ 'AuthenticatorISMG': authenticator_ISMG,
+ 'Version': version
+ }
+
+ @staticmethod
+ def submit_response_parse(body):
+ msg_id = body[:8]
+ result = struct.unpack('!B', body[8:9])
+ return {
+ 'Msg_Id': msg_id, 'Result': result[0]
+ }
+
+ @staticmethod
+ def deliver_request_parse(body):
+ msg_id, = struct.unpack('!Q', body[0:8])
+ dest_id = body[8:29]
+ service_id = body[29:39]
+ tp_pid = struct.unpack('!B', body[39:40])
+ tp_udhi = struct.unpack('!B', body[40:41])
+ msg_fmt = struct.unpack('!B', body[41:42])
+ src_terminal_id = body[42:63]
+ registered_delivery = struct.unpack('!B', body[63:64])
+ msg_length = struct.unpack('!B', body[64:65])
+ msg_content = body[65:msg_length[0]+65]
+ return {
+ 'Msg_Id': msg_id, 'Dest_Id': dest_id, 'Service_Id': service_id,
+ 'TP_pid': tp_pid, 'TP_udhi': tp_udhi, 'Msg_Fmt': msg_fmt,
+ 'Src_terminal_Id': src_terminal_id, 'Registered_Delivery': registered_delivery,
+ 'Msg_Length': msg_length, 'Msg_content': msg_content
+ }
+
+ def parse_header(self, data):
+ self.command_id, = struct.unpack('!L', data[4:8])
+ sequence_id, = struct.unpack('!L', data[8:12])
+ return {
+ 'length': self.length,
+ 'command_id': hex(self.command_id),
+ 'sequence_id': sequence_id
+ }
+
+ def parse_body(self, body):
+ response_body_func = self.response_handler_map.get(self.command_id)
+ if response_body_func is None:
+ raise JMSException('Unable to parse the returned result: %s' % body)
+ return response_body_func(body)
+
+ def parse(self, data):
+ self.length, = struct.unpack('!L', data[0:4])
+ header = self.parse_header(data)
+ body = self.parse_body(data[12:self.length])
+ return header, body
+
+
+class CMPPClient(object):
+ def __init__(self, host, port, sp_id, sp_secret, src_id, service_id):
+ self.ip = host
+ self.port = port
+ self.sp_id = sp_id
+ self.sp_secret = sp_secret
+ self.src_id = src_id
+ self.service_id = service_id
+ self._sequence_id = 0
+ self._is_connect = False
+ self._times = 3
+ self.__socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self._connect()
+
+ @property
+ def sequence_id(self):
+ s = self._sequence_id
+ self._sequence_id += 1
+ return s
+
+ def _connect(self):
+ self.__socket.settimeout(5)
+ error_msg = _('Failed to connect to the CMPP gateway server, err: {}')
+ for i in range(self._times):
+ try:
+ self.__socket.connect((self.ip, self.port))
+ except Exception as err:
+ error_msg = error_msg.format(str(err))
+ logger.warning(error_msg)
+ time.sleep(1)
+ else:
+ self._is_connect = True
+ break
+ else:
+ raise JMSException(error_msg)
+
+ def send(self, instance):
+ if isinstance(instance, CMPPBaseRequestInstance):
+ message = instance.get_message(sequence_id=self.sequence_id)
+ else:
+ message = instance
+ self.__socket.send(message)
+
+ def recv(self):
+ raw_length = self.__socket.recv(4)
+ length, = struct.unpack('!L', raw_length)
+ header, body = CMPPResponseInstance().parse(
+ raw_length + self.__socket.recv(length - 4)
+ )
+ return header, body
+
+ def close(self):
+ if self._is_connect:
+ terminate_request = CMPPTerminateRequestInstance()
+ self.send(terminate_request)
+ self.__socket.close()
+
+ def _cmpp_connect(self):
+ connect_request = CMPPConnectRequestInstance(self.sp_id, self.sp_secret)
+ self.send(connect_request)
+ header, body = self.recv()
+ if body['Status'] != 0:
+ raise JMSException('CMPPv2.0 authentication failed: %s' % body)
+
+ def _cmpp_send_sms(self, dest, sign_name, template_code, template_param):
+ """
+ 优先发送template_param中message的信息
+ 若该内容不存在,则根据template_code构建验证码发送
+ """
+ message = template_param.get('message')
+ if message is None:
+ code = template_param.get('code')
+ message = template_code.replace('{code}', code)
+ msg = '【%s】 %s' % (sign_name, message)
+ submit_request = CMPPSubmitRequestInstance(
+ msg_src=self.sp_id, src_id=self.src_id, msg_content=msg,
+ dest_usr_tl=len(dest), dest_terminal_id=dest,
+ service_id=self.service_id
+ )
+ self.send(submit_request)
+ header, body = self.recv()
+ command_id = header.get('command_id')
+ if command_id == CMPP_DELIVER:
+ deliver_request = CMPPDeliverRespRequestInstance(
+ msg_id=body['Msg_Id'], result=body['Result']
+ )
+ self.send(deliver_request)
+
+ def send_sms(self, dest, sign_name, template_code, template_param):
+ try:
+ self._cmpp_connect()
+ self._cmpp_send_sms(dest, sign_name, template_code, template_param)
+ except Exception as e:
+ logger.error('CMPPv2.0 Error: %s', e)
+ self.close()
+ raise JMSException(e)
+
+
+class CMPP2SMS(BaseSMSClient):
+ SIGN_AND_TMPL_SETTING_FIELD_PREFIX = 'CMPP2'
+
+ @classmethod
+ def new_from_settings(cls):
+ return cls(
+ host=settings.CMPP2_HOST, port=settings.CMPP2_PORT,
+ sp_id=settings.CMPP2_SP_ID, sp_secret=settings.CMPP2_SP_SECRET,
+ service_id=settings.CMPP2_SERVICE_ID, src_id=getattr(settings, 'CMPP2_SRC_ID', ''),
+ )
+
+ def __init__(self, host: str, port: int, sp_id: str, sp_secret: str, service_id: str, src_id=''):
+ try:
+ self.client = CMPPClient(
+ host=host, port=port, sp_id=sp_id, sp_secret=sp_secret, src_id=src_id, service_id=service_id
+ )
+ except Exception as err:
+ self.client = None
+ logger.warning(err)
+ raise JMSException(err)
+
+ @staticmethod
+ def need_pre_check():
+ return False
+
+ def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
+ try:
+ logger.info(f'CMPPv2.0 sms send: '
+ f'phone_numbers={phone_numbers} '
+ f'sign_name={sign_name} '
+ f'template_code={template_code} '
+ f'template_param={template_param}')
+ self.client.send_sms(phone_numbers, sign_name, template_code, template_param)
+ except Exception as e:
+ raise JMSException(e)
+
+
+client = CMPP2SMS
diff --git a/apps/common/sdk/sms/endpoint.py b/apps/common/sdk/sms/endpoint.py
index 610bf2d99..3bcaa8559 100644
--- a/apps/common/sdk/sms/endpoint.py
+++ b/apps/common/sdk/sms/endpoint.py
@@ -15,6 +15,7 @@ logger = get_logger(__name__)
class BACKENDS(TextChoices):
ALIBABA = 'alibaba', _('Alibaba cloud')
TENCENT = 'tencent', _('Tencent cloud')
+ CMPP2 = 'cmpp2', _('CMPP v2.0')
class SMS:
@@ -43,7 +44,7 @@ class SMS:
sign_name = getattr(settings, f'{self.client.SIGN_AND_TMPL_SETTING_FIELD_PREFIX}_VERIFY_SIGN_NAME')
template_code = getattr(settings, f'{self.client.SIGN_AND_TMPL_SETTING_FIELD_PREFIX}_VERIFY_TEMPLATE_CODE')
- if not (sign_name and template_code):
+ if self.client.need_pre_check() and not (sign_name and template_code):
raise JMSException(
code='verify_code_sign_tmpl_invalid',
detail=_('SMS verification code signature or template invalid')
diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py
index 854f9f488..93283e99b 100644
--- a/apps/common/utils/common.py
+++ b/apps/common/utils/common.py
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
#
import re
+import socket
+from django.templatetags.static import static
from collections import OrderedDict
from itertools import chain
import logging
@@ -381,3 +383,13 @@ def test_ip_connectivity(host, port, timeout=0.5):
else:
connectivity = False
return connectivity
+<<<<<<< HEAD
+=======
+
+
+def static_or_direct(logo_path):
+ if logo_path.startswith('img/'):
+ return static(logo_path)
+ else:
+ return logo_path
+>>>>>>> origin
diff --git a/apps/common/utils/crypto.py b/apps/common/utils/crypto.py
index 331f8b0bb..0a75ee4e3 100644
--- a/apps/common/utils/crypto.py
+++ b/apps/common/utils/crypto.py
@@ -1,9 +1,10 @@
import base64
import logging
+import re
from Cryptodome.Cipher import AES, PKCS1_v1_5
-from Cryptodome.Util.Padding import pad
from Cryptodome.Random import get_random_bytes
from Cryptodome.PublicKey import RSA
+from Cryptodome.Util.Padding import pad
from Cryptodome import Random
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
@@ -11,21 +12,25 @@ from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
-def process_key(key):
+secret_pattern = re.compile(r'password|secret|key|token', re.IGNORECASE)
+
+
+def padding_key(key, max_length=32):
"""
返回32 bytes 的key
"""
if not isinstance(key, bytes):
key = bytes(key, encoding='utf-8')
- if len(key) >= 32:
- return key[:32]
+ if len(key) >= max_length:
+ return key[:max_length]
- return pad(key, 32)
+ while len(key) % 16 != 0:
+ key += b'\0'
+ return key
class BaseCrypto:
-
def encrypt(self, text):
return base64.urlsafe_b64encode(
self._encrypt(bytes(text, encoding='utf8'))
@@ -45,7 +50,7 @@ class BaseCrypto:
class GMSM4EcbCrypto(BaseCrypto):
def __init__(self, key):
- self.key = process_key(key)
+ self.key = padding_key(key, 16)
self.sm4_encryptor = CryptSM4()
self.sm4_encryptor.set_key(self.key, SM4_ENCRYPT)
@@ -70,9 +75,8 @@ class AESCrypto:
"""
def __init__(self, key):
- if len(key) > 32:
- key = key[:32]
- self.key = self.to_16(key)
+ self.key = padding_key(key, 32)
+ self.aes = AES.new(self.key, AES.MODE_ECB)
@staticmethod
def to_16(key):
@@ -87,17 +91,15 @@ class AESCrypto:
return key # 返回bytes
def aes(self):
- return AES.new(self.key, AES.MODE_ECB) # 初始化加密器
+ return AES.new(self.key, AES.MODE_ECB)
def encrypt(self, text):
- aes = self.aes()
- cipher = base64.encodebytes(aes.encrypt(self.to_16(text)))
+ cipher = base64.encodebytes(self.aes.encrypt(self.to_16(text)))
return str(cipher, encoding='utf8').replace('\n', '') # 加密
def decrypt(self, text):
- aes = self.aes()
text_decoded = base64.decodebytes(bytes(text, encoding='utf8'))
- return str(aes.decrypt(text_decoded).rstrip(b'\0').decode("utf8"))
+ return str(self.aes.decrypt(text_decoded).rstrip(b'\0').decode("utf8"))
class AESCryptoGCM:
@@ -106,7 +108,15 @@ class AESCryptoGCM:
"""
def __init__(self, key):
- self.key = process_key(key)
+ self.key = self.process_key(key)
+
+ @staticmethod
+ def process_key(key):
+ if not isinstance(key, bytes):
+ key = bytes(key, encoding='utf-8')
+ if len(key) >= 32:
+ return key[:32]
+ return pad(key, 32)
def encrypt(self, text):
"""
@@ -133,7 +143,6 @@ class AESCryptoGCM:
nonce = base64.b64decode(metadata[24:48])
tag = base64.b64decode(metadata[48:])
ciphertext = base64.b64decode(text[72:])
-
cipher = AES.new(self.key, AES.MODE_GCM, nonce=nonce)
cipher.update(header)
@@ -144,11 +153,10 @@ class AESCryptoGCM:
def get_aes_crypto(key=None, mode='GCM'):
if key is None:
key = settings.SECRET_KEY
- if mode == 'ECB':
- a = AESCrypto(key)
- elif mode == 'GCM':
- a = AESCryptoGCM(key)
- return a
+ if mode == 'GCM':
+ return AESCryptoGCM(key)
+ else:
+ return AESCrypto(key)
def get_gm_sm4_ecb_crypto(key=None):
@@ -162,34 +170,42 @@ gm_sm4_ecb_crypto = get_gm_sm4_ecb_crypto()
class Crypto:
- cryptoes = {
+ cryptor_map = {
'aes_ecb': aes_ecb_crypto,
'aes_gcm': aes_crypto,
'aes': aes_crypto,
'gm_sm4_ecb': gm_sm4_ecb_crypto,
'gm': gm_sm4_ecb_crypto,
}
+ cryptos = []
def __init__(self):
- cryptoes = self.__class__.cryptoes.copy()
- crypto = cryptoes.pop(settings.SECURITY_DATA_CRYPTO_ALGO, None)
- if crypto is None:
+ crypt_algo = settings.SECURITY_DATA_CRYPTO_ALGO
+ if not crypt_algo:
+ if settings.GMSSL_ENABLED:
+ crypt_algo = 'gm'
+ else:
+ crypt_algo = 'aes'
+
+ cryptor = self.cryptor_map.get(crypt_algo, None)
+ if cryptor is None:
raise ImproperlyConfigured(
f'Crypto method not supported {settings.SECURITY_DATA_CRYPTO_ALGO}'
)
- self.cryptoes = [crypto, *cryptoes.values()]
+ others = set(self.cryptor_map.values()) - {cryptor}
+ self.cryptos = [cryptor, *others]
@property
def encryptor(self):
- return self.cryptoes[0]
+ return self.cryptos[0]
def encrypt(self, text):
return self.encryptor.encrypt(text)
def decrypt(self, text):
- for decryptor in self.cryptoes:
+ for cryptor in self.cryptos:
try:
- origin_text = decryptor.decrypt(text)
+ origin_text = cryptor.decrypt(text)
if origin_text:
# 有时不同算法解密不报错,但是返回空字符串
return origin_text
@@ -255,11 +271,13 @@ def decrypt_password(value):
if len(cipher) != 2:
return value
key_cipher, password_cipher = cipher
+ if not all([key_cipher, password_cipher]):
+ return value
aes_key = rsa_decrypt_by_session_pkey(key_cipher)
aes = get_aes_crypto(aes_key, 'ECB')
try:
password = aes.decrypt(password_cipher)
- except UnicodeDecodeError as e:
+ except Exception as e:
logging.error("Decrypt password error: {}, {}".format(password_cipher, e))
return value
return password
diff --git a/apps/common/utils/django.py b/apps/common/utils/django.py
index 3e8066cef..1f3f83282 100644
--- a/apps/common/utils/django.py
+++ b/apps/common/utils/django.py
@@ -8,12 +8,14 @@ from django.utils import timezone
from django.db import models
from django.db.models.signals import post_save, pre_save
-
UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}')
-def reverse(view_name, urlconf=None, args=None, kwargs=None,
- current_app=None, external=False, api_to_ui=False):
+def reverse(
+ view_name, urlconf=None, args=None, kwargs=None,
+ current_app=None, external=False, api_to_ui=False,
+ is_console=False, is_audit=False, is_workbench=False
+):
url = dj_reverse(view_name, urlconf=urlconf, args=args,
kwargs=kwargs, current_app=current_app)
@@ -21,7 +23,15 @@ def reverse(view_name, urlconf=None, args=None, kwargs=None,
site_url = settings.SITE_URL
url = site_url.strip('/') + url
if api_to_ui:
- url = url.replace('api/v1', 'ui/#').rstrip('/')
+ replace_str = 'ui/#'
+ if is_console:
+ replace_str += '/console'
+ elif is_audit:
+ replace_str += '/audit'
+ elif is_workbench:
+ replace_str += '/workbench'
+
+ url = url.replace('api/v1', replace_str).rstrip('/')
return url
@@ -38,7 +48,7 @@ def date_expired_default():
years = int(settings.DEFAULT_EXPIRED_YEARS)
except TypeError:
years = 70
- return timezone.now() + timezone.timedelta(days=365*years)
+ return timezone.now() + timezone.timedelta(days=365 * years)
def union_queryset(*args, base_queryset=None):
diff --git a/apps/common/utils/encode.py b/apps/common/utils/encode.py
index 4178e4a0d..2bf02ac4c 100644
--- a/apps/common/utils/encode.py
+++ b/apps/common/utils/encode.py
@@ -196,7 +196,8 @@ def encrypt_password(password, salt=None, algorithm='sha512'):
return des_crypt.hash(password, salt=salt[:2])
support_algorithm = {
- 'sha512': sha512, 'des': des
+ 'sha512': sha512,
+ 'des': des
}
if isinstance(algorithm, str):
@@ -222,9 +223,6 @@ def ensure_last_char_is_ascii(data):
remain = ''
-secret_pattern = re.compile(r'password|secret|key', re.IGNORECASE)
-
-
def data_to_json(data, sort_keys=True, indent=2, cls=None):
if cls is None:
cls = DjangoJSONEncoder
diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py
index 0eaaffad9..3fe4b89ff 100644
--- a/apps/jumpserver/conf.py
+++ b/apps/jumpserver/conf.py
@@ -15,18 +15,23 @@ import errno
import json
import yaml
import copy
+import base64
+import logging
from importlib import import_module
from urllib.parse import urljoin, urlparse
+from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
from django.urls import reverse_lazy
-from django.conf import settings
from django.utils.translation import ugettext_lazy as _
+
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_DIR = os.path.dirname(BASE_DIR)
XPACK_DIR = os.path.join(BASE_DIR, 'xpack')
HAS_XPACK = os.path.isdir(XPACK_DIR)
+logger = logging.getLogger('jumpserver.conf')
+
def import_string(dotted_path):
try:
@@ -39,9 +44,9 @@ def import_string(dotted_path):
try:
return getattr(module, class_name)
except AttributeError as err:
- raise ImportError('Module "%s" does not define a "%s" attribute/class' % (
- module_path, class_name)
- ) from err
+ raise ImportError(
+ 'Module "%s" does not define a "%s" attribute/class' %
+ (module_path, class_name)) from err
def is_absolute_uri(uri):
@@ -80,6 +85,59 @@ class DoesNotExist(Exception):
pass
+class ConfigCrypto:
+ secret_keys = [
+ 'SECRET_KEY', 'DB_PASSWORD', 'REDIS_PASSWORD',
+ ]
+
+ def __init__(self, key):
+ self.safe_key = self.process_key(key)
+ self.sm4_encryptor = CryptSM4()
+ self.sm4_encryptor.set_key(self.safe_key, SM4_ENCRYPT)
+
+ self.sm4_decryptor = CryptSM4()
+ self.sm4_decryptor.set_key(self.safe_key, SM4_DECRYPT)
+
+ @staticmethod
+ def process_key(secret_encrypt_key):
+ key = secret_encrypt_key.encode()
+ if len(key) >= 16:
+ key = key[:16]
+ else:
+ key += b'\0' * (16 - len(key))
+ return key
+
+ def encrypt(self, data):
+ data = bytes(data, encoding='utf8')
+ return base64.b64encode(self.sm4_encryptor.crypt_ecb(data)).decode('utf8')
+
+ def decrypt(self, data):
+ data = base64.urlsafe_b64decode(bytes(data, encoding='utf8'))
+ return self.sm4_decryptor.crypt_ecb(data).decode('utf8')
+
+ def decrypt_if_need(self, value, item):
+ if item not in self.secret_keys:
+ return value
+
+ try:
+ plaintext = self.decrypt(value)
+ if plaintext:
+ value = plaintext
+ except Exception as e:
+ logger.error('decrypt %s error: %s', item, e)
+ return value
+
+ @classmethod
+ def get_secret_encryptor(cls):
+ # 使用 SM4 加密配置文件敏感信息
+ # https://the-x.cn/cryptography/Sm4.aspx
+ secret_encrypt_key = os.environ.get('SECRET_ENCRYPT_KEY', '')
+ if not secret_encrypt_key:
+ return None
+ print('Info: Using SM4 to encrypt config secret value')
+ return cls(secret_encrypt_key)
+
+
class Config(dict):
"""Works exactly like a dict but provides ways to fill it from files
or special dictionaries. There are two common patterns to populate the
@@ -160,7 +218,7 @@ class Config(dict):
'SESSION_COOKIE_DOMAIN': None,
'CSRF_COOKIE_DOMAIN': None,
'SESSION_COOKIE_NAME_PREFIX': None,
- 'SESSION_COOKIE_AGE': 3600,
+ 'SESSION_COOKIE_AGE': 3600 * 24,
'SESSION_EXPIRE_AT_BROWSER_CLOSE': False,
'LOGIN_URL': reverse_lazy('authentication:login'),
'CONNECTION_TOKEN_EXPIRATION': 5 * 60,
@@ -265,6 +323,22 @@ class Config(dict):
'AUTH_SAML2_PROVIDER_AUTHORIZATION_ENDPOINT': '/',
'AUTH_SAML2_AUTHENTICATION_FAILURE_REDIRECT_URI': '/',
+ # OAuth2 认证
+ 'AUTH_OAUTH2': False,
+ 'AUTH_OAUTH2_LOGO_PATH': 'img/login_oauth2_logo.png',
+ 'AUTH_OAUTH2_PROVIDER': 'OAuth2',
+ 'AUTH_OAUTH2_ALWAYS_UPDATE_USER': True,
+ 'AUTH_OAUTH2_CLIENT_ID': 'client-id',
+ 'AUTH_OAUTH2_SCOPE': '',
+ 'AUTH_OAUTH2_CLIENT_SECRET': '',
+ 'AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT': 'https://oauth2.example.com/authorize',
+ 'AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT': 'https://oauth2.example.com/userinfo',
+ 'AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT': 'https://oauth2.example.com/access_token',
+ 'AUTH_OAUTH2_ACCESS_TOKEN_METHOD': 'GET',
+ 'AUTH_OAUTH2_USER_ATTR_MAP': {
+ 'name': 'name', 'username': 'username', 'email': 'email'
+ },
+
'AUTH_TEMP_TOKEN': False,
# 企业微信
@@ -302,6 +376,15 @@ class Config(dict):
'TENCENT_VERIFY_SIGN_NAME': '',
'TENCENT_VERIFY_TEMPLATE_CODE': '',
+ 'CMPP2_HOST': '',
+ 'CMPP2_PORT': 7890,
+ 'CMPP2_SP_ID': '',
+ 'CMPP2_SP_SECRET': '',
+ 'CMPP2_SRC_ID': '',
+ 'CMPP2_SERVICE_ID': '',
+ 'CMPP2_VERIFY_SIGN_NAME': '',
+ 'CMPP2_VERIFY_TEMPLATE_CODE': '{code}',
+
# Email
'EMAIL_CUSTOM_USER_CREATED_SUBJECT': _('Create account successfully'),
'EMAIL_CUSTOM_USER_CREATED_HONORIFIC': _('Hello'),
@@ -387,7 +470,8 @@ class Config(dict):
'SESSION_SAVE_EVERY_REQUEST': True,
'SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE': False,
'SERVER_REPLAY_STORAGE': {},
- 'SECURITY_DATA_CRYPTO_ALGO': 'aes',
+ 'SECURITY_DATA_CRYPTO_ALGO': None,
+ 'GMSSL_ENABLED': False,
# 记录清理清理
'LOGIN_LOG_KEEP_DAYS': 200,
@@ -405,6 +489,7 @@ class Config(dict):
'CONNECTION_TOKEN_ENABLED': False,
'PERM_SINGLE_ASSET_TO_UNGROUP_NODE': False,
+ 'TICKET_AUTHORIZE_DEFAULT_TIME': 7,
'WINDOWS_SSH_DEFAULT_SHELL': 'cmd',
'PERIOD_TASK_ENABLED': True,
@@ -416,6 +501,10 @@ class Config(dict):
'HEALTH_CHECK_TOKEN': '',
}
+ def __init__(self, *args):
+ super().__init__(*args)
+ self.secret_encryptor = ConfigCrypto.get_secret_encryptor()
+
@staticmethod
def convert_keycloak_to_openid(keycloak_config):
"""
@@ -427,7 +516,6 @@ class Config(dict):
"""
openid_config = copy.deepcopy(keycloak_config)
-
auth_openid = openid_config.get('AUTH_OPENID')
auth_openid_realm_name = openid_config.get('AUTH_OPENID_REALM_NAME')
auth_openid_server_url = openid_config.get('AUTH_OPENID_SERVER_URL')
@@ -556,13 +644,12 @@ class Config(dict):
def get(self, item):
# 再从配置文件中获取
value = self.get_from_config(item)
- if value is not None:
- return value
- # 其次从环境变量来
- value = self.get_from_env(item)
- if value is not None:
- return value
- value = self.defaults.get(item)
+ if value is None:
+ value = self.get_from_env(item)
+ if value is None:
+ value = self.defaults.get(item)
+ if self.secret_encryptor:
+ value = self.secret_encryptor.decrypt_if_need(value, item)
return value
def __getitem__(self, item):
diff --git a/apps/jumpserver/context_processor.py b/apps/jumpserver/context_processor.py
index a02f38838..414144d39 100644
--- a/apps/jumpserver/context_processor.py
+++ b/apps/jumpserver/context_processor.py
@@ -11,7 +11,7 @@ default_interface = dict((
('favicon', static('img/facio.ico')),
('login_title', _('JumpServer Open Source Bastion Host')),
('theme', 'classic_green'),
- ('theme_info', None),
+ ('theme_info', {}),
))
default_context = {
diff --git a/apps/jumpserver/routing.py b/apps/jumpserver/routing.py
index 1d1de2230..773baee99 100644
--- a/apps/jumpserver/routing.py
+++ b/apps/jumpserver/routing.py
@@ -4,10 +4,10 @@ from django.core.asgi import get_asgi_application
from ops.urls.ws_urls import urlpatterns as ops_urlpatterns
from notifications.urls.ws_urls import urlpatterns as notifications_urlpatterns
+from settings.urls.ws_urls import urlpatterns as setting_urlpatterns
urlpatterns = []
-urlpatterns += ops_urlpatterns \
- + notifications_urlpatterns
+urlpatterns += ops_urlpatterns + notifications_urlpatterns + setting_urlpatterns
application = ProtocolTypeRouter({
'websocket': AuthMiddlewareStack(
diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py
index 95a12d01f..de43b03be 100644
--- a/apps/jumpserver/settings/auth.py
+++ b/apps/jumpserver/settings/auth.py
@@ -24,9 +24,15 @@ AUTH_LDAP_GLOBAL_OPTIONS = {
ldap.OPT_X_TLS_REQUIRE_CERT: ldap.OPT_X_TLS_NEVER,
ldap.OPT_REFERRALS: CONFIG.AUTH_LDAP_OPTIONS_OPT_REFERRALS
}
-LDAP_CERT_FILE = os.path.join(PROJECT_DIR, "data", "certs", "ldap_ca.pem")
+LDAP_CACERT_FILE = os.path.join(PROJECT_DIR, "data", "certs", "ldap_ca.pem")
+if os.path.isfile(LDAP_CACERT_FILE):
+ AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_X_TLS_CACERTFILE] = LDAP_CACERT_FILE
+LDAP_CERT_FILE = os.path.join(PROJECT_DIR, "data", "certs", "ldap_cert.pem")
if os.path.isfile(LDAP_CERT_FILE):
- AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_X_TLS_CACERTFILE] = LDAP_CERT_FILE
+ AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_X_TLS_CERTFILE] = LDAP_CERT_FILE
+LDAP_KEY_FILE = os.path.join(PROJECT_DIR, "data", "certs", "ldap_cert.key")
+if os.path.isfile(LDAP_KEY_FILE):
+ AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_X_TLS_KEYFILE] = LDAP_KEY_FILE
# AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU
# AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER
# AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
@@ -143,6 +149,23 @@ SAML2_SP_ADVANCED_SETTINGS = CONFIG.SAML2_SP_ADVANCED_SETTINGS
SAML2_LOGIN_URL_NAME = "authentication:saml2:saml2-login"
SAML2_LOGOUT_URL_NAME = "authentication:saml2:saml2-logout"
+# OAuth2 auth
+AUTH_OAUTH2 = CONFIG.AUTH_OAUTH2
+AUTH_OAUTH2_LOGO_PATH = CONFIG.AUTH_OAUTH2_LOGO_PATH
+AUTH_OAUTH2_PROVIDER = CONFIG.AUTH_OAUTH2_PROVIDER
+AUTH_OAUTH2_ALWAYS_UPDATE_USER = CONFIG.AUTH_OAUTH2_ALWAYS_UPDATE_USER
+AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT = CONFIG.AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT
+AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT = CONFIG.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT
+AUTH_OAUTH2_ACCESS_TOKEN_METHOD = CONFIG.AUTH_OAUTH2_ACCESS_TOKEN_METHOD
+AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT = CONFIG.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT
+AUTH_OAUTH2_CLIENT_SECRET = CONFIG.AUTH_OAUTH2_CLIENT_SECRET
+AUTH_OAUTH2_CLIENT_ID = CONFIG.AUTH_OAUTH2_CLIENT_ID
+AUTH_OAUTH2_SCOPE = CONFIG.AUTH_OAUTH2_SCOPE
+AUTH_OAUTH2_USER_ATTR_MAP = CONFIG.AUTH_OAUTH2_USER_ATTR_MAP
+AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME = 'authentication:oauth2:login-callback'
+AUTH_OAUTH2_AUTHENTICATION_REDIRECT_URI = '/'
+AUTH_OAUTH2_AUTHENTICATION_FAILURE_REDIRECT_URI = '/'
+
# 临时 token
AUTH_TEMP_TOKEN = CONFIG.AUTH_TEMP_TOKEN
@@ -170,6 +193,7 @@ AUTH_BACKEND_DINGTALK = 'authentication.backends.sso.DingTalkAuthentication'
AUTH_BACKEND_FEISHU = 'authentication.backends.sso.FeiShuAuthentication'
AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.sso.AuthorizationTokenAuthentication'
AUTH_BACKEND_SAML2 = 'authentication.backends.saml2.SAML2Backend'
+AUTH_BACKEND_OAUTH2 = 'authentication.backends.oauth2.OAuth2Backend'
AUTH_BACKEND_TEMP_TOKEN = 'authentication.backends.token.TempTokenAuthBackend'
@@ -180,12 +204,14 @@ AUTHENTICATION_BACKENDS = [
AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY, AUTH_BACKEND_LDAP, AUTH_BACKEND_RADIUS,
# 跳转形式
AUTH_BACKEND_CAS, AUTH_BACKEND_OIDC_PASSWORD, AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_SAML2,
+ AUTH_BACKEND_OAUTH2,
# 扫码模式
AUTH_BACKEND_WECOM, AUTH_BACKEND_DINGTALK, AUTH_BACKEND_FEISHU,
# Token模式
AUTH_BACKEND_AUTH_TOKEN, AUTH_BACKEND_SSO, AUTH_BACKEND_TEMP_TOKEN
]
+AUTHENTICATION_BACKENDS_THIRD_PARTY = [AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_CAS, AUTH_BACKEND_SAML2, AUTH_BACKEND_OAUTH2]
ONLY_ALLOW_EXIST_USER_AUTH = CONFIG.ONLY_ALLOW_EXIST_USER_AUTH
ONLY_ALLOW_AUTH_FROM_SOURCE = CONFIG.ONLY_ALLOW_AUTH_FROM_SOURCE
diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py
index 519623ac9..a65266a1f 100644
--- a/apps/jumpserver/settings/base.py
+++ b/apps/jumpserver/settings/base.py
@@ -43,6 +43,9 @@ DEBUG_DEV = CONFIG.DEBUG_DEV
# Absolute url for some case, for example email link
SITE_URL = CONFIG.SITE_URL
+# https://docs.djangoproject.com/en/4.1/ref/settings/
+SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
+
# LOG LEVEL
LOG_LEVEL = CONFIG.LOG_LEVEL
@@ -106,6 +109,7 @@ MIDDLEWARE = [
'authentication.backends.oidc.middleware.OIDCRefreshIDTokenMiddleware',
'authentication.backends.cas.middleware.CASMiddleware',
'authentication.middleware.MFAMiddleware',
+ 'authentication.middleware.ThirdPartyLoginMiddleware',
'authentication.middleware.SessionCookieMiddleware',
'simple_history.middleware.HistoryRequestMiddleware',
]
@@ -307,6 +311,21 @@ CSRF_COOKIE_SECURE = CONFIG.CSRF_COOKIE_SECURE
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
+PASSWORD_HASHERS = [
+ 'django.contrib.auth.hashers.PBKDF2PasswordHasher',
+ 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
+ 'django.contrib.auth.hashers.Argon2PasswordHasher',
+ 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
+]
+
+
+GMSSL_ENABLED = CONFIG.GMSSL_ENABLED
+GM_HASHER = 'common.hashers.PBKDF2SM3PasswordHasher'
+if GMSSL_ENABLED:
+ PASSWORD_HASHERS.insert(0, GM_HASHER)
+else:
+ PASSWORD_HASHERS.append(GM_HASHER)
+
# For Debug toolbar
INTERNAL_IPS = ["127.0.0.1"]
if os.environ.get('DEBUG_TOOLBAR', False):
@@ -315,3 +334,4 @@ if os.environ.get('DEBUG_TOOLBAR', False):
DEBUG_TOOLBAR_PANELS = [
'debug_toolbar.panels.profiling.ProfilingPanel',
]
+
diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py
index 28c740189..5ae6feaa7 100644
--- a/apps/jumpserver/settings/custom.py
+++ b/apps/jumpserver/settings/custom.py
@@ -84,6 +84,7 @@ TERMINAL_TELNET_REGEX = CONFIG.TERMINAL_TELNET_REGEX
BACKEND_ASSET_USER_AUTH_VAULT = False
PERM_SINGLE_ASSET_TO_UNGROUP_NODE = CONFIG.PERM_SINGLE_ASSET_TO_UNGROUP_NODE
+TICKET_AUTHORIZE_DEFAULT_TIME = CONFIG.TICKET_AUTHORIZE_DEFAULT_TIME
PERM_EXPIRED_CHECK_PERIODIC = CONFIG.PERM_EXPIRED_CHECK_PERIODIC
WINDOWS_SSH_DEFAULT_SHELL = CONFIG.WINDOWS_SSH_DEFAULT_SHELL
FLOWER_URL = CONFIG.FLOWER_URL
diff --git a/apps/locale/ja/LC_MESSAGES/django.mo b/apps/locale/ja/LC_MESSAGES/django.mo
index 5625428c7..9fc9c6640 100644
--- a/apps/locale/ja/LC_MESSAGES/django.mo
+++ b/apps/locale/ja/LC_MESSAGES/django.mo
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7b79695fe8cb323097c12171db8f6ae58b8e016b317f08562183b677f537e8b3
-size 129597
+oid sha256:261eee68117787809a9bc6b2034846ee7b222677224f97055f7d7398d427b1d7
+size 255
diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po
index 9205fb974..a5cb194ad 100644
--- a/apps/locale/ja/LC_MESSAGES/django.po
+++ b/apps/locale/ja/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-07-12 17:58+0800\n"
+"POT-Creation-Date: 2022-08-17 16:28+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -23,27 +23,27 @@ msgid "Acls"
msgstr "Acls"
#: acls/models/base.py:25 acls/serializers/login_asset_acl.py:47
-#: applications/models/application.py:217 assets/models/asset.py:138
+#: applications/models/application.py:220 assets/models/asset.py:138
#: assets/models/base.py:175 assets/models/cluster.py:18
#: assets/models/cmd_filter.py:27 assets/models/domain.py:23
#: assets/models/group.py:20 assets/models/label.py:18 ops/mixin.py:24
-#: orgs/models.py:65 perms/models/base.py:83 rbac/models/role.py:29
-#: settings/models.py:29 settings/serializers/sms.py:6
-#: terminal/models/endpoint.py:10 terminal/models/endpoint.py:82
+#: orgs/models.py:70 perms/models/base.py:83 rbac/models/role.py:29
+#: settings/models.py:33 settings/serializers/sms.py:6
+#: terminal/models/endpoint.py:10 terminal/models/endpoint.py:86
#: terminal/models/storage.py:26 terminal/models/task.py:16
#: terminal/models/terminal.py:100 users/forms/profile.py:33
-#: users/models/group.py:15 users/models/user.py:661
+#: users/models/group.py:15 users/models/user.py:665
#: xpack/plugins/cloud/models.py:28
msgid "Name"
msgstr "名前"
#: acls/models/base.py:27 assets/models/cmd_filter.py:84
-#: assets/models/user.py:251 terminal/models/endpoint.py:85
+#: assets/models/user.py:251 terminal/models/endpoint.py:89
msgid "Priority"
msgstr "優先順位"
#: acls/models/base.py:28 assets/models/cmd_filter.py:84
-#: assets/models/user.py:251 terminal/models/endpoint.py:86
+#: assets/models/user.py:251 terminal/models/endpoint.py:90
msgid "1-100, the lower the value will be match first"
msgstr "1-100、低い値は最初に一致します"
@@ -53,18 +53,18 @@ msgstr "1-100、低い値は最初に一致します"
msgid "Active"
msgstr "アクティブ"
-#: acls/models/base.py:32 applications/models/application.py:230
+#: acls/models/base.py:32 applications/models/application.py:233
#: assets/models/asset.py:143 assets/models/asset.py:231
#: assets/models/backup.py:54 assets/models/base.py:180
#: assets/models/cluster.py:29 assets/models/cmd_filter.py:48
#: assets/models/cmd_filter.py:96 assets/models/domain.py:24
#: assets/models/domain.py:65 assets/models/group.py:23
-#: assets/models/label.py:23 ops/models/adhoc.py:38 orgs/models.py:68
-#: perms/models/base.py:93 rbac/models/role.py:37 settings/models.py:34
-#: terminal/models/endpoint.py:21 terminal/models/endpoint.py:92
+#: assets/models/label.py:23 ops/models/adhoc.py:38 orgs/models.py:73
+#: perms/models/base.py:93 rbac/models/role.py:37 settings/models.py:38
+#: terminal/models/endpoint.py:23 terminal/models/endpoint.py:96
#: terminal/models/storage.py:29 terminal/models/terminal.py:114
#: tickets/models/comment.py:32 tickets/models/ticket/general.py:288
-#: users/models/group.py:16 users/models/user.py:698
+#: users/models/group.py:16 users/models/user.py:702
#: xpack/plugins/change_auth_plan/models/base.py:44
#: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:116
#: xpack/plugins/gathered_user/models.py:26
@@ -80,7 +80,7 @@ msgstr "拒否"
msgid "Allow"
msgstr "許可"
-#: acls/models/login_acl.py:20 acls/models/login_acl.py:104
+#: acls/models/login_acl.py:20 acls/models/login_acl.py:75
#: acls/models/login_asset_acl.py:17 tickets/const.py:9
msgid "Login confirm"
msgstr "ログイン確認"
@@ -88,13 +88,13 @@ msgstr "ログイン確認"
#: acls/models/login_acl.py:24 acls/models/login_asset_acl.py:20
#: assets/models/cmd_filter.py:30 assets/models/label.py:15 audits/models.py:37
#: audits/models.py:62 audits/models.py:87 audits/serializers.py:100
-#: authentication/models.py:54 authentication/models.py:78 orgs/models.py:214
-#: perms/models/base.py:84 rbac/builtin.py:118 rbac/models/rolebinding.py:41
+#: authentication/models.py:54 authentication/models.py:78 orgs/models.py:220
+#: perms/models/base.py:84 rbac/builtin.py:120 rbac/models/rolebinding.py:41
#: terminal/backends/command/models.py:20
#: terminal/backends/command/serializers.py:13 terminal/models/session.py:44
#: terminal/models/sharing.py:33 terminal/notifications.py:91
#: terminal/notifications.py:139 tickets/models/comment.py:21 users/const.py:14
-#: users/models/user.py:890 users/models/user.py:921
+#: users/models/user.py:894 users/models/user.py:925
#: users/serializers/group.py:19
msgid "User"
msgstr "ユーザー"
@@ -157,14 +157,14 @@ msgstr "コンマ区切り文字列の形式。* はすべて一致すること
#: acls/serializers/login_asset_acl.py:51 assets/models/base.py:176
#: assets/models/gathered_user.py:15 audits/models.py:121
#: authentication/forms.py:25 authentication/forms.py:27
-#: authentication/models.py:245
+#: authentication/models.py:260
#: authentication/templates/authentication/_msg_different_city.html:9
#: authentication/templates/authentication/_msg_oauth_bind.html:9
-#: ops/models/adhoc.py:159 users/forms/profile.py:32 users/models/user.py:659
+#: ops/models/adhoc.py:159 users/forms/profile.py:32 users/models/user.py:663
#: users/templates/users/_msg_user_created.html:12
#: xpack/plugins/change_auth_plan/models/asset.py:34
#: xpack/plugins/change_auth_plan/models/asset.py:195
-#: xpack/plugins/cloud/serializers/account_attrs.py:24
+#: xpack/plugins/cloud/serializers/account_attrs.py:26
msgid "Username"
msgstr "ユーザー名"
@@ -185,7 +185,7 @@ msgstr ""
#: authentication/templates/authentication/_msg_oauth_bind.html:12
#: authentication/templates/authentication/_msg_rest_password_success.html:8
#: authentication/templates/authentication/_msg_rest_public_key_success.html:8
-#: settings/serializers/terminal.py:8 terminal/serializers/endpoint.py:42
+#: settings/serializers/terminal.py:8 terminal/serializers/endpoint.py:54
msgid "IP"
msgstr "IP"
@@ -214,7 +214,7 @@ msgid "Unsupported protocols: {}"
msgstr "サポートされていないプロトコル: {}"
#: acls/serializers/login_asset_acl.py:98
-#: tickets/serializers/ticket/ticket.py:78
+#: tickets/serializers/ticket/ticket.py:85
msgid "The organization `{}` does not exist"
msgstr "組織 '{}'は存在しません"
@@ -241,7 +241,7 @@ msgstr ""
msgid "Time Period"
msgstr "期間"
-#: applications/apps.py:9 applications/models/application.py:63
+#: applications/apps.py:9 applications/models/application.py:64
msgid "Applications"
msgstr "アプリケーション"
@@ -260,7 +260,11 @@ msgstr "リモートアプリ"
msgid "Custom"
msgstr "カスタム"
-#: applications/models/account.py:12 applications/models/application.py:234
+#: applications/const.py:91 rbac/tree.py:29
+msgid "Other"
+msgstr "その他"
+
+#: applications/models/account.py:12 applications/models/application.py:237
#: assets/models/backup.py:32 assets/models/cmd_filter.py:45
#: authentication/models.py:67 authentication/models.py:95
#: perms/models/application_permission.py:28
@@ -278,8 +282,9 @@ msgstr "アプリケーション"
msgid "System user"
msgstr "システムユーザー"
-#: applications/models/account.py:17 assets/models/authbook.py:21
-#: settings/serializers/auth/cas.py:18
+#: applications/models/account.py:17
+#: applications/serializers/attrs/application_type/oracle.py:13
+#: assets/models/authbook.py:21 settings/serializers/auth/cas.py:18
msgid "Version"
msgstr "バージョン"
@@ -295,44 +300,44 @@ msgstr "アプリケーションアカウントの秘密を表示できます"
msgid "Can change application account secret"
msgstr "アプリケーションアカウントの秘密を変更できます"
-#: applications/models/application.py:219
+#: applications/models/application.py:222
#: applications/serializers/application.py:99 assets/models/label.py:21
#: perms/models/application_permission.py:21
#: perms/serializers/application/user_permission.py:33
-#: tickets/models/ticket/apply_application.py:14
+#: tickets/models/ticket/apply_application.py:15
#: xpack/plugins/change_auth_plan/models/app.py:25
msgid "Category"
msgstr "カテゴリ"
-#: applications/models/application.py:222
+#: applications/models/application.py:225
#: applications/serializers/application.py:101 assets/models/backup.py:49
#: assets/models/cmd_filter.py:82 assets/models/user.py:250
#: authentication/models.py:70 perms/models/application_permission.py:24
#: perms/serializers/application/user_permission.py:34
#: terminal/models/storage.py:58 terminal/models/storage.py:143
#: tickets/models/comment.py:26 tickets/models/flow.py:57
-#: tickets/models/ticket/apply_application.py:17
+#: tickets/models/ticket/apply_application.py:18
#: tickets/models/ticket/general.py:273
#: xpack/plugins/change_auth_plan/models/app.py:28
#: xpack/plugins/change_auth_plan/models/app.py:153
msgid "Type"
msgstr "タイプ"
-#: applications/models/application.py:226 assets/models/asset.py:217
+#: applications/models/application.py:229 assets/models/asset.py:217
#: assets/models/domain.py:29 assets/models/domain.py:64
msgid "Domain"
msgstr "ドメイン"
-#: applications/models/application.py:228 xpack/plugins/cloud/models.py:33
-#: xpack/plugins/cloud/serializers/account.py:60
+#: applications/models/application.py:231 xpack/plugins/cloud/models.py:33
+#: xpack/plugins/cloud/serializers/account.py:61
msgid "Attrs"
msgstr "ツールバーの"
-#: applications/models/application.py:238
+#: applications/models/application.py:241
msgid "Can match application"
msgstr "アプリケーションを一致させることができます"
-#: applications/models/application.py:305
+#: applications/models/application.py:320
msgid "Application user"
msgstr "アプリケーションユーザー"
@@ -347,7 +352,7 @@ msgstr "カテゴリ表示"
#: assets/serializers/cmd_filter.py:34 assets/serializers/system_user.py:34
#: audits/serializers.py:29 authentication/serializers/connection_token.py:22
#: perms/serializers/application/permission.py:19
-#: tickets/serializers/flow.py:48 tickets/serializers/ticket/ticket.py:17
+#: tickets/serializers/flow.py:49 tickets/serializers/ticket/ticket.py:17
msgid "Type display"
msgstr "タイプ表示"
@@ -358,8 +363,8 @@ msgstr "タイプ表示"
#: assets/serializers/account.py:18 assets/serializers/cmd_filter.py:28
#: assets/serializers/cmd_filter.py:48 common/db/models.py:114
#: common/mixins/models.py:50 ops/models/adhoc.py:39 ops/models/command.py:30
-#: orgs/models.py:67 orgs/models.py:217 perms/models/base.py:92
-#: users/models/group.py:18 users/models/user.py:922
+#: orgs/models.py:72 orgs/models.py:223 perms/models/base.py:92
+#: users/models/group.py:18 users/models/user.py:926
#: xpack/plugins/cloud/models.py:125
msgid "Date created"
msgstr "作成された日付"
@@ -368,7 +373,7 @@ msgstr "作成された日付"
#: assets/models/gathered_user.py:20 assets/serializers/account.py:21
#: assets/serializers/cmd_filter.py:29 assets/serializers/cmd_filter.py:49
#: common/db/models.py:115 common/mixins/models.py:51 ops/models/adhoc.py:40
-#: orgs/models.py:218
+#: orgs/models.py:224
msgid "Date updated"
msgstr "更新日"
@@ -388,8 +393,8 @@ msgstr "クラスター"
#: applications/serializers/attrs/application_category/db.py:11
#: ops/models/adhoc.py:157 settings/serializers/auth/radius.py:14
-#: terminal/models/endpoint.py:11
-#: xpack/plugins/cloud/serializers/account_attrs.py:70
+#: settings/serializers/auth/sms.py:56 terminal/models/endpoint.py:11
+#: xpack/plugins/cloud/serializers/account_attrs.py:72
msgid "Host"
msgstr "ホスト"
@@ -397,16 +402,20 @@ msgstr "ホスト"
#: applications/serializers/attrs/application_type/mongodb.py:10
#: applications/serializers/attrs/application_type/mysql.py:10
#: applications/serializers/attrs/application_type/mysql_workbench.py:22
-#: applications/serializers/attrs/application_type/oracle.py:10
+#: applications/serializers/attrs/application_type/oracle.py:16
#: applications/serializers/attrs/application_type/pgsql.py:10
#: applications/serializers/attrs/application_type/redis.py:10
#: applications/serializers/attrs/application_type/sqlserver.py:10
#: assets/models/asset.py:214 assets/models/domain.py:62
-#: settings/serializers/auth/radius.py:15
-#: xpack/plugins/cloud/serializers/account_attrs.py:71
+#: settings/serializers/auth/radius.py:15 settings/serializers/auth/sms.py:57
+#: xpack/plugins/cloud/serializers/account_attrs.py:73
msgid "Port"
msgstr "ポート"
+#: applications/serializers/attrs/application_category/remote_app.py:34
+msgid "Asset Info"
+msgstr "資産情報"
+
#: applications/serializers/attrs/application_category/remote_app.py:39
#: applications/serializers/attrs/application_type/chrome.py:14
#: applications/serializers/attrs/application_type/mysql_workbench.py:14
@@ -416,13 +425,13 @@ msgstr "アプリケーションパス"
#: applications/serializers/attrs/application_category/remote_app.py:44
#: assets/serializers/system_user.py:167
-#: tickets/serializers/ticket/apply_application.py:35
+#: tickets/serializers/ticket/apply_application.py:38
#: tickets/serializers/ticket/common.py:59
#: xpack/plugins/change_auth_plan/serializers/asset.py:67
#: xpack/plugins/change_auth_plan/serializers/asset.py:70
#: xpack/plugins/change_auth_plan/serializers/asset.py:73
#: xpack/plugins/change_auth_plan/serializers/asset.py:104
-#: xpack/plugins/cloud/serializers/account_attrs.py:54
+#: xpack/plugins/cloud/serializers/account_attrs.py:56
msgid "This field is required."
msgstr "このフィールドは必須です。"
@@ -467,6 +476,10 @@ msgstr "Mysql workbench のユーザー名"
msgid "Mysql workbench password"
msgstr "Mysql workbench パスワード"
+#: applications/serializers/attrs/application_type/oracle.py:14
+msgid "Magnus currently supports only 11g and 12c connections"
+msgstr "現在、Magnusは11gおよび12cバージョンへの接続のみをサポートしています"
+
#: applications/serializers/attrs/application_type/vmware_client.py:26
msgid "Vmware username"
msgstr "Vmware ユーザー名"
@@ -516,6 +529,7 @@ msgstr "内部"
#: assets/models/asset.py:162 assets/models/asset.py:216
#: assets/serializers/account.py:15 assets/serializers/asset.py:63
#: perms/serializers/asset/user_permission.py:43
+#: xpack/plugins/cloud/serializers/account_attrs.py:162
msgid "Platform"
msgstr "プラットフォーム"
@@ -614,8 +628,8 @@ msgstr "ラベル"
#: assets/models/asset.py:229 assets/models/base.py:183
#: assets/models/cluster.py:28 assets/models/cmd_filter.py:52
#: assets/models/cmd_filter.py:99 assets/models/group.py:21
-#: common/db/models.py:112 common/mixins/models.py:49 orgs/models.py:66
-#: orgs/models.py:219 perms/models/base.py:91 users/models/user.py:706
+#: common/db/models.py:112 common/mixins/models.py:49 orgs/models.py:71
+#: orgs/models.py:225 perms/models/base.py:91 users/models/user.py:710
#: users/serializers/group.py:33
#: xpack/plugins/change_auth_plan/models/base.py:48
#: xpack/plugins/cloud/models.py:122 xpack/plugins/gathered_user/models.py:30
@@ -698,7 +712,7 @@ msgstr "タイミングトリガー"
#: assets/models/backup.py:105 audits/models.py:44 ops/models/command.py:31
#: perms/models/base.py:89 terminal/models/session.py:58
-#: tickets/models/ticket/apply_application.py:25
+#: tickets/models/ticket/apply_application.py:29
#: tickets/models/ticket/apply_asset.py:23
#: xpack/plugins/change_auth_plan/models/base.py:112
#: xpack/plugins/change_auth_plan/models/base.py:203
@@ -726,7 +740,7 @@ msgid "Trigger mode"
msgstr "トリガーモード"
#: assets/models/backup.py:119 audits/models.py:127
-#: terminal/models/sharing.py:106
+#: terminal/models/sharing.py:108
#: xpack/plugins/change_auth_plan/models/base.py:201
#: xpack/plugins/change_auth_plan/serializers/app.py:66
#: xpack/plugins/change_auth_plan/serializers/asset.py:180
@@ -760,7 +774,7 @@ msgstr "OK"
#: assets/models/base.py:32 audits/models.py:118
#: xpack/plugins/change_auth_plan/serializers/app.py:88
#: xpack/plugins/change_auth_plan/serializers/asset.py:199
-#: xpack/plugins/cloud/const.py:32
+#: xpack/plugins/cloud/const.py:33
msgid "Failed"
msgstr "失敗しました"
@@ -768,7 +782,7 @@ msgstr "失敗しました"
msgid "Connectivity"
msgstr "接続性"
-#: assets/models/base.py:40 authentication/models.py:248
+#: assets/models/base.py:40 authentication/models.py:263
msgid "Date verified"
msgstr "確認済みの日付"
@@ -786,7 +800,7 @@ msgstr "確認済みの日付"
#: xpack/plugins/change_auth_plan/models/base.py:196
#: xpack/plugins/change_auth_plan/serializers/base.py:21
#: xpack/plugins/change_auth_plan/serializers/base.py:73
-#: xpack/plugins/cloud/serializers/account_attrs.py:26
+#: xpack/plugins/cloud/serializers/account_attrs.py:28
msgid "Password"
msgstr "パスワード"
@@ -811,7 +825,7 @@ msgstr "帯域幅"
msgid "Contact"
msgstr "連絡先"
-#: assets/models/cluster.py:22 users/models/user.py:681
+#: assets/models/cluster.py:22 users/models/user.py:685
msgid "Phone"
msgstr "電話"
@@ -837,7 +851,7 @@ msgid "Default"
msgstr "デフォルト"
#: assets/models/cluster.py:36 assets/models/label.py:14 rbac/const.py:6
-#: users/models/user.py:907
+#: users/models/user.py:911
msgid "System"
msgstr "システム"
@@ -846,7 +860,7 @@ msgid "Default Cluster"
msgstr "デフォルトクラスター"
#: assets/models/cmd_filter.py:34 perms/models/base.py:86
-#: users/models/group.py:31 users/models/user.py:667
+#: users/models/group.py:31 users/models/user.py:671
msgid "User group"
msgstr "ユーザーグループ"
@@ -894,11 +908,11 @@ msgstr "家を無視する"
msgid "Command filter rule"
msgstr "コマンドフィルタルール"
-#: assets/models/cmd_filter.py:144
+#: assets/models/cmd_filter.py:147
msgid "The generated regular expression is incorrect: {}"
msgstr "生成された正規表現が正しくありません: {}"
-#: assets/models/cmd_filter.py:170 tickets/const.py:13
+#: assets/models/cmd_filter.py:173 tickets/const.py:13
msgid "Command confirm"
msgstr "コマンドの確認"
@@ -915,7 +929,8 @@ msgstr "テストゲートウェイ"
msgid "Unable to connect to port {port} on {ip}"
msgstr "{ip} でポート {port} に接続できません"
-#: assets/models/domain.py:134 xpack/plugins/cloud/providers/fc.py:48
+#: assets/models/domain.py:134 authentication/middleware.py:75
+#: xpack/plugins/cloud/providers/fc.py:48
msgid "Authentication failed"
msgstr "認証に失敗しました"
@@ -947,7 +962,7 @@ msgstr "資産グループ"
msgid "Default asset group"
msgstr "デフォルトアセットグループ"
-#: assets/models/label.py:19 assets/models/node.py:546 settings/models.py:30
+#: assets/models/label.py:19 assets/models/node.py:553 settings/models.py:34
msgid "Value"
msgstr "値"
@@ -955,32 +970,32 @@ msgstr "値"
msgid "Label"
msgstr "ラベル"
-#: assets/models/node.py:151
+#: assets/models/node.py:158
msgid "New node"
msgstr "新しいノード"
-#: assets/models/node.py:474
+#: assets/models/node.py:481
msgid "empty"
msgstr "空"
-#: assets/models/node.py:545 perms/models/asset_permission.py:101
+#: assets/models/node.py:552 perms/models/asset_permission.py:101
msgid "Key"
msgstr "キー"
-#: assets/models/node.py:547 assets/serializers/node.py:20
+#: assets/models/node.py:554 assets/serializers/node.py:20
msgid "Full value"
msgstr "フルバリュー"
-#: assets/models/node.py:550 perms/models/asset_permission.py:102
+#: assets/models/node.py:557 perms/models/asset_permission.py:102
msgid "Parent key"
msgstr "親キー"
-#: assets/models/node.py:559 assets/serializers/system_user.py:267
+#: assets/models/node.py:566 assets/serializers/system_user.py:267
#: xpack/plugins/cloud/models.py:96 xpack/plugins/cloud/serializers/task.py:70
msgid "Node"
msgstr "ノード"
-#: assets/models/node.py:562
+#: assets/models/node.py:569
msgid "Can match node"
msgstr "ノードを一致させることができます"
@@ -1128,6 +1143,7 @@ msgstr "CPU情報"
#: perms/serializers/application/permission.py:42
#: perms/serializers/asset/permission.py:18
#: perms/serializers/asset/permission.py:46
+#: tickets/models/ticket/apply_application.py:27
#: tickets/models/ticket/apply_asset.py:21
msgid "Actions"
msgstr "アクション"
@@ -1143,7 +1159,7 @@ msgstr "定期的なパフォーマンス"
msgid "Currently only mail sending is supported"
msgstr "現在、メール送信のみがサポートされています"
-#: assets/serializers/base.py:16 users/models/user.py:689
+#: assets/serializers/base.py:16 users/models/user.py:693
msgid "Private key"
msgstr "ssh秘密鍵"
@@ -1426,7 +1442,7 @@ msgid "Symlink"
msgstr "Symlink"
#: audits/models.py:38 audits/models.py:66 audits/models.py:89
-#: terminal/models/session.py:51 terminal/models/sharing.py:94
+#: terminal/models/session.py:51 terminal/models/sharing.py:96
msgid "Remote addr"
msgstr "リモートaddr"
@@ -1438,7 +1454,7 @@ msgstr "操作"
msgid "Filename"
msgstr "ファイル名"
-#: audits/models.py:43 audits/models.py:117 terminal/models/sharing.py:102
+#: audits/models.py:43 audits/models.py:117 terminal/models/sharing.py:104
#: tickets/views/approve.py:115
#: xpack/plugins/change_auth_plan/serializers/app.py:87
#: xpack/plugins/change_auth_plan/serializers/asset.py:198
@@ -1493,7 +1509,7 @@ msgstr "パスワード変更ログ"
msgid "Disabled"
msgstr "無効"
-#: audits/models.py:112 settings/models.py:33
+#: audits/models.py:112 settings/models.py:37
msgid "Enabled"
msgstr "有効化"
@@ -1521,7 +1537,7 @@ msgstr "ユーザーエージェント"
#: audits/models.py:126
#: authentication/templates/authentication/_mfa_confirm_modal.html:14
-#: users/forms/profile.py:65 users/models/user.py:684
+#: users/forms/profile.py:65 users/models/user.py:688
#: users/serializers/profile.py:126
msgid "MFA"
msgstr "MFA"
@@ -1599,24 +1615,24 @@ msgid "Auth Token"
msgstr "認証トークン"
#: audits/signal_handlers.py:53 authentication/notifications.py:73
-#: authentication/views/login.py:67 authentication/views/wecom.py:178
-#: notifications/backends/__init__.py:11 users/models/user.py:720
+#: authentication/views/login.py:73 authentication/views/wecom.py:178
+#: notifications/backends/__init__.py:11 users/models/user.py:724
msgid "WeCom"
msgstr "企業微信"
#: audits/signal_handlers.py:54 authentication/views/feishu.py:144
-#: authentication/views/login.py:79 notifications/backends/__init__.py:14
-#: users/models/user.py:722
+#: authentication/views/login.py:85 notifications/backends/__init__.py:14
+#: users/models/user.py:726
msgid "FeiShu"
msgstr "本を飛ばす"
#: audits/signal_handlers.py:55 authentication/views/dingtalk.py:179
-#: authentication/views/login.py:73 notifications/backends/__init__.py:12
-#: users/models/user.py:721
+#: authentication/views/login.py:79 notifications/backends/__init__.py:12
+#: users/models/user.py:725
msgid "DingTalk"
msgstr "DingTalk"
-#: audits/signal_handlers.py:56 authentication/models.py:252
+#: audits/signal_handlers.py:56 authentication/models.py:267
msgid "Temporary token"
msgstr "仮パスワード"
@@ -1853,6 +1869,10 @@ msgstr ""
msgid "Invalid token or cache refreshed."
msgstr "無効なトークンまたはキャッシュの更新。"
+#: authentication/backends/oauth2/backends.py:155 authentication/models.py:158
+msgid "User invalid, disabled or expired"
+msgstr "ユーザーが無効、無効、または期限切れです"
+
#: authentication/confirm/password.py:16
msgid "Authentication failed password incorrect"
msgstr "認証に失敗しました (ユーザー名またはパスワードが正しくありません)"
@@ -1903,7 +1923,7 @@ msgstr "Authバックエンドが一致しない"
#: authentication/errors/const.py:28
msgid "ACL is not allowed"
-msgstr "ACLは許可されません"
+msgstr "ログイン アクセス制御は許可されません"
#: authentication/errors/const.py:29
msgid "Only local users are allowed"
@@ -1969,23 +1989,19 @@ msgstr "受け入れのためのログイン確認チケットを待つ"
msgid "Login confirm ticket was {}"
msgstr "ログイン確認チケットは {} でした"
-#: authentication/errors/failed.py:145
-msgid "IP is not allowed"
-msgstr "IPは許可されていません"
+#: authentication/errors/failed.py:146
+msgid "Current IP and Time period is not allowed"
+msgstr "現在の IP と期間はログインを許可されていません"
-#: authentication/errors/failed.py:152
-msgid "Time Period is not allowed"
-msgstr "期間は許可されていません"
-
-#: authentication/errors/failed.py:157
+#: authentication/errors/failed.py:151
msgid "Please enter MFA code"
msgstr "MFAコードを入力してください"
-#: authentication/errors/failed.py:162
+#: authentication/errors/failed.py:156
msgid "Please enter SMS code"
msgstr "SMSコードを入力してください"
-#: authentication/errors/failed.py:167 users/exceptions.py:15
+#: authentication/errors/failed.py:161 users/exceptions.py:15
msgid "Phone not set"
msgstr "電話が設定されていない"
@@ -1993,19 +2009,37 @@ msgstr "電話が設定されていない"
msgid "SSO auth closed"
msgstr "SSO authは閉鎖されました"
+#: authentication/errors/mfa.py:18 authentication/views/wecom.py:80
+msgid "WeCom is already bound"
+msgstr "企業の微信はすでにバインドされています"
+
+#: authentication/errors/mfa.py:23 authentication/views/wecom.py:237
+#: authentication/views/wecom.py:291
+msgid "WeCom is not bound"
+msgstr "企業の微信をバインドしていません"
+
+#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:242
+#: authentication/views/dingtalk.py:296
+msgid "DingTalk is not bound"
+msgstr "DingTalkはバインドされていません"
+
+#: authentication/errors/mfa.py:33 authentication/views/feishu.py:203
+msgid "FeiShu is not bound"
+msgstr "本を飛ばすは拘束されていません"
+
#: authentication/errors/mfa.py:38
msgid "Your password is invalid"
msgstr "パスワードが無効です"
-#: authentication/errors/redirect.py:79 authentication/mixins.py:306
+#: authentication/errors/redirect.py:85 authentication/mixins.py:306
msgid "Your password is too simple, please change it for security"
msgstr "パスワードがシンプルすぎるので、セキュリティのために変更してください"
-#: authentication/errors/redirect.py:87 authentication/mixins.py:313
+#: authentication/errors/redirect.py:93 authentication/mixins.py:313
msgid "You should to change your password before login"
msgstr "ログインする前にパスワードを変更する必要があります"
-#: authentication/errors/redirect.py:95 authentication/mixins.py:320
+#: authentication/errors/redirect.py:101 authentication/mixins.py:320
msgid "Your password has expired, please reset before logging in"
msgstr ""
"パスワードの有効期限が切れました。ログインする前にリセットしてください。"
@@ -2082,6 +2116,10 @@ msgstr "電話番号を設定して有効にする"
msgid "Clear phone number to disable"
msgstr "無効にする電話番号をクリアする"
+#: authentication/middleware.py:76 settings/utils/ldap.py:652
+msgid "Authentication failed (before login check failed): {}"
+msgstr "認証に失敗しました (ログインチェックが失敗する前): {}"
+
#: authentication/mixins.py:256
msgid "The MFA type ({}) is not enabled"
msgstr "MFAタイプ ({}) が有効になっていない"
@@ -2106,15 +2144,15 @@ msgstr "期限切れ"
msgid "SSO token"
msgstr "SSO token"
-#: authentication/models.py:72 authentication/models.py:246
+#: authentication/models.py:72 authentication/models.py:261
#: authentication/templates/authentication/_access_key_modal.html:31
#: settings/serializers/auth/radius.py:17
msgid "Secret"
msgstr "ひみつ"
-#: authentication/models.py:74 authentication/models.py:249
-#: perms/models/base.py:90 tickets/models/ticket/apply_application.py:26
-#: tickets/models/ticket/apply_asset.py:24 users/models/user.py:703
+#: authentication/models.py:74 authentication/models.py:264
+#: perms/models/base.py:90 tickets/models/ticket/apply_application.py:30
+#: tickets/models/ticket/apply_asset.py:24 users/models/user.py:707
msgid "Date expired"
msgstr "期限切れの日付"
@@ -2130,51 +2168,47 @@ msgstr "接続トークン"
msgid "Can view connection token secret"
msgstr "接続トークンの秘密を表示できます"
-#: authentication/models.py:141
+#: authentication/models.py:149
msgid "Connection token expired at: {}"
msgstr "接続トークンの有効期限: {}"
-#: authentication/models.py:146
+#: authentication/models.py:154
msgid "User not exists"
msgstr "ユーザーは存在しません"
-#: authentication/models.py:150
-msgid "User invalid, disabled or expired"
-msgstr "ユーザーが無効、無効、または期限切れです"
-
-#: authentication/models.py:155
+#: authentication/models.py:163
msgid "System user not exists"
msgstr "システムユーザーが存在しません"
-#: authentication/models.py:161
+#: authentication/models.py:169
msgid "Asset not exists"
msgstr "アセットが存在しません"
-#: authentication/models.py:165
+#: authentication/models.py:173
msgid "Asset inactive"
msgstr "アセットがアクティブ化されていません"
-#: authentication/models.py:172
+#: authentication/models.py:180
msgid "User has no permission to access asset or permission expired"
msgstr ""
"ユーザーがアセットにアクセスする権限を持っていないか、権限の有効期限が切れて"
"います"
-#: authentication/models.py:180
+#: authentication/models.py:188
msgid "Application not exists"
msgstr "アプリが存在しません"
-#: authentication/models.py:187
+#: authentication/models.py:195
msgid "User has no permission to access application or permission expired"
msgstr ""
"ユーザーがアプリにアクセスする権限を持っていないか、権限の有効期限が切れてい"
"ます"
-#: authentication/models.py:247
+#: authentication/models.py:262
msgid "Verified"
msgstr "確認済み"
-#: authentication/models.py:268
+#: authentication/models.py:283
msgid "Super connection token"
msgstr "スーパー接続トークン"
@@ -2191,6 +2225,10 @@ msgstr "バインディングリマインダー"
msgid "Validity"
msgstr "有効性"
+#: authentication/serializers/connection_token.py:24
+msgid "Expired time"
+msgstr "期限切れ時間"
+
#: authentication/serializers/connection_token.py:73
msgid "Asset or application required"
msgstr "アセットまたはアプリが必要"
@@ -2271,6 +2309,7 @@ msgid "Need MFA for view auth"
msgstr "ビューオートのためにMFAが必要"
#: authentication/templates/authentication/_mfa_confirm_modal.html:20
+#: authentication/templates/authentication/auth_fail_flash_message_standalone.html:37
#: templates/_modal.html:23 templates/flash_message_standalone.html:37
#: users/templates/users/user_password_verify.html:20
msgid "Confirm"
@@ -2285,7 +2324,7 @@ msgstr "コードエラー"
#: authentication/templates/authentication/_msg_reset_password.html:3
#: authentication/templates/authentication/_msg_rest_password_success.html:2
#: authentication/templates/authentication/_msg_rest_public_key_success.html:2
-#: jumpserver/conf.py:307 ops/tasks.py:145 ops/tasks.py:148
+#: jumpserver/conf.py:390 ops/tasks.py:145 ops/tasks.py:148
#: perms/templates/perms/_msg_item_permissions_expire.html:3
#: perms/templates/perms/_msg_permed_items_expire.html:3
#: tickets/templates/tickets/approve_check_password.html:33
@@ -2376,14 +2415,19 @@ msgstr ""
"公開鍵の更新が開始されなかった場合、アカウントにセキュリティ上の問題がある可"
"能性があります"
+#: authentication/templates/authentication/auth_fail_flash_message_standalone.html:28
+#: templates/flash_message_standalone.html:28 tickets/const.py:20
+msgid "Cancel"
+msgstr "キャンセル"
+
#: authentication/templates/authentication/login.html:221
msgid "Welcome back, please enter username and password to login"
msgstr ""
"おかえりなさい、ログインするためにユーザー名とパスワードを入力してください"
#: authentication/templates/authentication/login.html:256
-#: users/templates/users/forgot_password.html:15
#: users/templates/users/forgot_password.html:16
+#: users/templates/users/forgot_password.html:17
msgid "Forgot password"
msgstr "パスワードを忘れた"
@@ -2424,10 +2468,15 @@ msgstr "リンクのコピー"
msgid "Return"
msgstr "返品"
-#: authentication/templates/authentication/login_wait_confirm.html:113
+#: authentication/templates/authentication/login_wait_confirm.html:116
msgid "Copy success"
msgstr "コピー成功"
+#: authentication/utils.py:28 common/utils/ip/geoip/utils.py:24
+#: xpack/plugins/cloud/const.py:24
+msgid "LAN"
+msgstr "ローカルエリアネットワーク"
+
#: authentication/views/dingtalk.py:41
msgid "DingTalk Error, Please contact your system administrator"
msgstr "DingTalkエラー、システム管理者に連絡してください"
@@ -2466,10 +2515,6 @@ msgstr "DingTalkのバインドに成功"
msgid "Failed to get user from DingTalk"
msgstr "DingTalkからユーザーを取得できませんでした"
-#: authentication/views/dingtalk.py:242 authentication/views/dingtalk.py:296
-msgid "DingTalk is not bound"
-msgstr "DingTalkはバインドされていません"
-
#: authentication/views/dingtalk.py:243 authentication/views/dingtalk.py:297
msgid "Please login with a password and then bind the DingTalk"
msgstr "パスワードでログインし、DingTalkをバインドしてください"
@@ -2498,27 +2543,23 @@ msgstr "本を飛ばすのバインドに成功"
msgid "Failed to get user from FeiShu"
msgstr "本を飛ばすからユーザーを取得できませんでした"
-#: authentication/views/feishu.py:203
-msgid "FeiShu is not bound"
-msgstr "本を飛ばすは拘束されていません"
-
#: authentication/views/feishu.py:204
msgid "Please login with a password and then bind the FeiShu"
msgstr "パスワードでログインしてから本を飛ばすをバインドしてください"
-#: authentication/views/login.py:175
+#: authentication/views/login.py:181
msgid "Redirecting"
msgstr "リダイレクト"
-#: authentication/views/login.py:176
+#: authentication/views/login.py:182
msgid "Redirecting to {} authentication"
msgstr "{} 認証へのリダイレクト"
-#: authentication/views/login.py:199
+#: authentication/views/login.py:205
msgid "Please enable cookies and try again."
msgstr "クッキーを有効にして、もう一度お試しください。"
-#: authentication/views/login.py:301
+#: authentication/views/login.py:307
msgid ""
"Wait for {} confirm, You also can copy link to her/him
\n"
" Don't close this page"
@@ -2526,15 +2567,15 @@ msgstr ""
"{} 確認を待ちます。彼女/彼へのリンクをコピーすることもできます
\n"
" このページを閉じないでください"
-#: authentication/views/login.py:306
+#: authentication/views/login.py:312
msgid "No ticket found"
msgstr "チケットが見つかりません"
-#: authentication/views/login.py:340
+#: authentication/views/login.py:346
msgid "Logout success"
msgstr "ログアウト成功"
-#: authentication/views/login.py:341
+#: authentication/views/login.py:347
msgid "Logout success, return login page"
msgstr "ログアウト成功、ログインページを返す"
@@ -2546,10 +2587,6 @@ msgstr "企業微信エラー、システム管理者に連絡してください
msgid "WeCom Error"
msgstr "企業微信エラー"
-#: authentication/views/wecom.py:80
-msgid "WeCom is already bound"
-msgstr "企業の微信はすでにバインドされています"
-
#: authentication/views/wecom.py:163
msgid "WeCom query user failed"
msgstr "企業微信ユーザーの問合せに失敗しました"
@@ -2566,10 +2603,6 @@ msgstr "企業の微信のバインドに成功"
msgid "Failed to get user from WeCom"
msgstr "企業の微信からユーザーを取得できませんでした"
-#: authentication/views/wecom.py:237 authentication/views/wecom.py:291
-msgid "WeCom is not bound"
-msgstr "企業の微信をバインドしていません"
-
#: authentication/views/wecom.py:238 authentication/views/wecom.py:292
msgid "Please login with a password and then bind the WeCom"
msgstr "パスワードでログインしてからWeComをバインドしてください"
@@ -2698,6 +2731,14 @@ msgstr "企業微信エラー、システム管理者に連絡してください
msgid "Signature does not match"
msgstr "署名が一致しない"
+#: common/sdk/sms/cmpp2.py:46
+msgid "sp_id is 6 bits"
+msgstr "SP idは6ビット"
+
+#: common/sdk/sms/cmpp2.py:216
+msgid "Failed to connect to the CMPP gateway server, err: {}"
+msgstr "接続ゲートウェイサーバエラー, 非: {}"
+
#: common/sdk/sms/endpoint.py:16
msgid "Alibaba cloud"
msgstr "アリ雲"
@@ -2706,11 +2747,15 @@ msgstr "アリ雲"
msgid "Tencent cloud"
msgstr "テンセント雲"
-#: common/sdk/sms/endpoint.py:28
+#: common/sdk/sms/endpoint.py:18
+msgid "CMPP v2.0"
+msgstr "CMPP v2.0"
+
+#: common/sdk/sms/endpoint.py:29
msgid "SMS provider not support: {}"
msgstr "SMSプロバイダーはサポートしていません: {}"
-#: common/sdk/sms/endpoint.py:49
+#: common/sdk/sms/endpoint.py:50
msgid "SMS verification code signature or template invalid"
msgstr "SMS検証コードの署名またはテンプレートが無効"
@@ -2726,10 +2771,6 @@ msgstr "確認コードが正しくありません"
msgid "Please wait {} seconds before sending"
msgstr "{} 秒待ってから送信してください"
-#: common/utils/ip/geoip/utils.py:24
-msgid "LAN"
-msgstr "LAN"
-
#: common/utils/ip/geoip/utils.py:26 common/utils/ip/utils.py:78
msgid "Invalid ip"
msgstr "無効なIP"
@@ -2746,11 +2787,11 @@ msgstr "特殊文字を含むべきではない"
msgid "The mobile phone number format is incorrect"
msgstr "携帯電話番号の形式が正しくありません"
-#: jumpserver/conf.py:306
+#: jumpserver/conf.py:389
msgid "Create account successfully"
msgstr "アカウントを正常に作成"
-#: jumpserver/conf.py:308
+#: jumpserver/conf.py:391
msgid "Your account has been created successfully"
msgstr "アカウントが正常に作成されました"
@@ -2795,7 +2836,7 @@ msgid "Notifications"
msgstr "通知"
#: notifications/backends/__init__.py:10 users/forms/profile.py:102
-#: users/models/user.py:663
+#: users/models/user.py:667
msgid "Email"
msgstr "メール"
@@ -3009,23 +3050,27 @@ msgstr "組織のリソース ({}) は削除できません"
msgid "App organizations"
msgstr "アプリ組織"
-#: orgs/mixins/models.py:46 orgs/mixins/serializers.py:25 orgs/models.py:80
-#: orgs/models.py:211 rbac/const.py:7 rbac/models/rolebinding.py:48
+#: orgs/mixins/models.py:56 orgs/mixins/serializers.py:25 orgs/models.py:85
+#: orgs/models.py:217 rbac/const.py:7 rbac/models/rolebinding.py:48
#: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:62
-#: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:64
+#: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:71
msgid "Organization"
msgstr "組織"
-#: orgs/models.py:74
+#: orgs/models.py:79
msgid "GLOBAL"
msgstr "グローバル組織"
-#: orgs/models.py:82
+#: orgs/models.py:87
msgid "Can view root org"
msgstr "グローバル組織を表示できます"
-#: orgs/models.py:216 rbac/models/role.py:46 rbac/models/rolebinding.py:44
-#: users/models/user.py:671
+#: orgs/models.py:88
+msgid "Can view all joined org"
+msgstr "参加しているすべての組織を表示できます"
+
+#: orgs/models.py:222 rbac/models/role.py:46 rbac/models/rolebinding.py:44
+#: users/models/user.py:675
msgid "Role"
msgstr "ロール"
@@ -3113,27 +3158,32 @@ msgstr "クリップボードコピーペースト"
msgid "From ticket"
msgstr "チケットから"
-#: perms/notifications.py:18
+#: perms/notifications.py:12 perms/notifications.py:44
+#: perms/notifications.py:88 perms/notifications.py:119
+msgid "today"
+msgstr "今"
+
+#: perms/notifications.py:15
msgid "You permed assets is about to expire"
msgstr "パーマ資産の有効期限が近づいています"
-#: perms/notifications.py:23
+#: perms/notifications.py:20
msgid "permed assets"
msgstr "パーマ資産"
-#: perms/notifications.py:62
+#: perms/notifications.py:59
msgid "Asset permissions is about to expire"
msgstr "資産権限の有効期限が近づいています"
-#: perms/notifications.py:67
+#: perms/notifications.py:64
msgid "asset permissions of organization {}"
msgstr "組織 {} の資産権限"
-#: perms/notifications.py:94
+#: perms/notifications.py:91
msgid "Your permed applications is about to expire"
msgstr "パーマアプリケーションの有効期限が近づいています"
-#: perms/notifications.py:98
+#: perms/notifications.py:95
msgid "permed applications"
msgstr "Permedアプリケーション"
@@ -3245,27 +3295,27 @@ msgstr "{} 少なくとも1つのシステムロール"
msgid "RBAC"
msgstr "RBAC"
-#: rbac/builtin.py:109
+#: rbac/builtin.py:111
msgid "SystemAdmin"
msgstr "システム管理者"
-#: rbac/builtin.py:112
+#: rbac/builtin.py:114
msgid "SystemAuditor"
msgstr "システム監査人"
-#: rbac/builtin.py:115
+#: rbac/builtin.py:117
msgid "SystemComponent"
msgstr "システムコンポーネント"
-#: rbac/builtin.py:121
+#: rbac/builtin.py:123
msgid "OrgAdmin"
msgstr "組織管理者"
-#: rbac/builtin.py:124
+#: rbac/builtin.py:126
msgid "OrgAuditor"
msgstr "監査員を組織する"
-#: rbac/builtin.py:127
+#: rbac/builtin.py:129
msgid "OrgUser"
msgstr "組織ユーザー"
@@ -3298,6 +3348,7 @@ msgid "Permission"
msgstr "権限"
#: rbac/models/role.py:31 rbac/models/rolebinding.py:38
+#: settings/serializers/auth/oauth2.py:35
msgid "Scope"
msgstr "スコープ"
@@ -3321,18 +3372,22 @@ msgstr "組織の役割"
msgid "Role binding"
msgstr "ロールバインディング"
-#: rbac/models/rolebinding.py:159
+#: rbac/models/rolebinding.py:137
+msgid "All organizations"
+msgstr "全ての組織"
+
+#: rbac/models/rolebinding.py:166
msgid ""
"User last role in org, can not be delete, you can remove user from org "
"instead"
msgstr ""
"ユーザーの最後のロールは削除できません。ユーザーを組織から削除できます。"
-#: rbac/models/rolebinding.py:166
+#: rbac/models/rolebinding.py:173
msgid "Organization role binding"
msgstr "組織の役割バインディング"
-#: rbac/models/rolebinding.py:181
+#: rbac/models/rolebinding.py:188
msgid "System role binding"
msgstr "システムロールバインディング"
@@ -3372,14 +3427,10 @@ msgstr "ワークスペースビュー"
msgid "Audit view"
msgstr "監査ビュー"
-#: rbac/tree.py:28 settings/models.py:140
+#: rbac/tree.py:28 settings/models.py:156
msgid "System setting"
msgstr "システム設定"
-#: rbac/tree.py:29
-msgid "Other"
-msgstr "その他"
-
#: rbac/tree.py:37
msgid "Accounts"
msgstr "アカウント"
@@ -3436,13 +3487,8 @@ msgstr "権限ツリーの表示"
msgid "Execute batch command"
msgstr "バッチ実行コマンド"
-#: settings/api/alibaba_sms.py:31 settings/api/tencent_sms.py:35
-msgid "test_phone is required"
-msgstr "携帯番号をテストこのフィールドは必須です"
-
-#: settings/api/alibaba_sms.py:52 settings/api/dingtalk.py:31
-#: settings/api/feishu.py:36 settings/api/tencent_sms.py:57
-#: settings/api/wecom.py:37
+#: settings/api/dingtalk.py:31 settings/api/feishu.py:36
+#: settings/api/sms.py:131 settings/api/wecom.py:37
msgid "Test success"
msgstr "テストの成功"
@@ -3470,47 +3516,55 @@ msgstr "Ldapユーザーを取得するにはNone"
msgid "Imported {} users successfully (Organization: {})"
msgstr "{} 人のユーザーを正常にインポートしました (組織: {})"
+#: settings/api/sms.py:113
+msgid "Invalid SMS platform"
+msgstr "無効なショートメッセージプラットフォーム"
+
+#: settings/api/sms.py:119
+msgid "test_phone is required"
+msgstr "携帯番号をテストこのフィールドは必須です"
+
#: settings/apps.py:7
msgid "Settings"
msgstr "設定"
-#: settings/models.py:142
+#: settings/models.py:158
msgid "Can change email setting"
msgstr "メール設定を変更できます"
-#: settings/models.py:143
+#: settings/models.py:159
msgid "Can change auth setting"
msgstr "資格認定の設定"
-#: settings/models.py:144
+#: settings/models.py:160
msgid "Can change system msg sub setting"
msgstr "システムmsgサブ设定を変更できます"
-#: settings/models.py:145
+#: settings/models.py:161
msgid "Can change sms setting"
msgstr "Smsの設定を変えることができます"
-#: settings/models.py:146
+#: settings/models.py:162
msgid "Can change security setting"
msgstr "セキュリティ設定を変更できます"
-#: settings/models.py:147
+#: settings/models.py:163
msgid "Can change clean setting"
msgstr "きれいな設定を変えることができます"
-#: settings/models.py:148
+#: settings/models.py:164
msgid "Can change interface setting"
msgstr "インターフェイスの設定を変えることができます"
-#: settings/models.py:149
+#: settings/models.py:165
msgid "Can change license setting"
msgstr "ライセンス設定を変更できます"
-#: settings/models.py:150
+#: settings/models.py:166
msgid "Can change terminal setting"
msgstr "ターミナルの設定を変えることができます"
-#: settings/models.py:151
+#: settings/models.py:167
msgid "Can change other setting"
msgstr "他の設定を変えることができます"
@@ -3623,7 +3677,8 @@ msgstr "ユーザー検索フィルター"
msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)"
msgstr "選択は (cnまたはuidまたはsAMAccountName)=%(user)s)"
-#: settings/serializers/auth/ldap.py:57 settings/serializers/auth/oidc.py:36
+#: settings/serializers/auth/ldap.py:57 settings/serializers/auth/oauth2.py:51
+#: settings/serializers/auth/oidc.py:36
msgid "User attr map"
msgstr "ユーザー属性マッピング"
@@ -3647,23 +3702,52 @@ msgstr "ページサイズを検索"
msgid "Enable LDAP auth"
msgstr "LDAP認証の有効化"
-#: settings/serializers/auth/oidc.py:15
-msgid "Base site url"
-msgstr "ベースサイトのアドレス"
+#: settings/serializers/auth/oauth2.py:20
+msgid "Enable OAuth2 Auth"
+msgstr "OAuth2認証の有効化"
-#: settings/serializers/auth/oidc.py:18
+#: settings/serializers/auth/oauth2.py:23
+msgid "Logo"
+msgstr "アイコン"
+
+#: settings/serializers/auth/oauth2.py:26
+msgid "Service provider"
+msgstr "サービスプロバイダー"
+
+#: settings/serializers/auth/oauth2.py:29 settings/serializers/auth/oidc.py:18
msgid "Client Id"
msgstr "クライアントID"
-#: settings/serializers/auth/oidc.py:21
-#: xpack/plugins/cloud/serializers/account_attrs.py:36
+#: settings/serializers/auth/oauth2.py:32 settings/serializers/auth/oidc.py:21
+#: xpack/plugins/cloud/serializers/account_attrs.py:38
msgid "Client Secret"
msgstr "クライアント秘密"
-#: settings/serializers/auth/oidc.py:29
+#: settings/serializers/auth/oauth2.py:38 settings/serializers/auth/oidc.py:62
+msgid "Provider auth endpoint"
+msgstr "認証エンドポイントアドレス"
+
+#: settings/serializers/auth/oauth2.py:41 settings/serializers/auth/oidc.py:65
+msgid "Provider token endpoint"
+msgstr "プロバイダートークンエンドポイント"
+
+#: settings/serializers/auth/oauth2.py:44 settings/serializers/auth/oidc.py:29
msgid "Client authentication method"
msgstr "クライアント認証方式"
+#: settings/serializers/auth/oauth2.py:48 settings/serializers/auth/oidc.py:71
+msgid "Provider userinfo endpoint"
+msgstr "プロバイダーuserinfoエンドポイント"
+
+#: settings/serializers/auth/oauth2.py:54 settings/serializers/auth/oidc.py:92
+#: settings/serializers/auth/saml2.py:33
+msgid "Always update user"
+msgstr "常にユーザーを更新"
+
+#: settings/serializers/auth/oidc.py:15
+msgid "Base site url"
+msgstr "ベースサイトのアドレス"
+
#: settings/serializers/auth/oidc.py:31
msgid "Share session"
msgstr "セッションの共有"
@@ -3696,22 +3780,10 @@ msgstr "OIDC認証の有効化"
msgid "Provider endpoint"
msgstr "プロバイダーエンドポイント"
-#: settings/serializers/auth/oidc.py:62
-msgid "Provider auth endpoint"
-msgstr "認証エンドポイントアドレス"
-
-#: settings/serializers/auth/oidc.py:65
-msgid "Provider token endpoint"
-msgstr "プロバイダートークンエンドポイント"
-
#: settings/serializers/auth/oidc.py:68
msgid "Provider jwks endpoint"
msgstr "プロバイダーjwksエンドポイント"
-#: settings/serializers/auth/oidc.py:71
-msgid "Provider userinfo endpoint"
-msgstr "プロバイダーuserinfoエンドポイント"
-
#: settings/serializers/auth/oidc.py:74
msgid "Provider end session endpoint"
msgstr "プロバイダーのセッション終了エンドポイント"
@@ -3744,10 +3816,6 @@ msgstr "使用状態"
msgid "Use nonce"
msgstr "Nonceを使用"
-#: settings/serializers/auth/oidc.py:92 settings/serializers/auth/saml2.py:33
-msgid "Always update user"
-msgstr "常にユーザーを更新"
-
#: settings/serializers/auth/radius.py:13
msgid "Enable Radius Auth"
msgstr "Radius認証の有効化"
@@ -3780,42 +3848,84 @@ msgstr "SP プライベートキー"
msgid "SP cert"
msgstr "SP 証明書"
-#: settings/serializers/auth/sms.py:11
+#: settings/serializers/auth/sms.py:15
msgid "Enable SMS"
msgstr "SMSの有効化"
-#: settings/serializers/auth/sms.py:13
-msgid "SMS provider"
-msgstr "SMSプロバイダ"
+#: settings/serializers/auth/sms.py:17
+msgid "SMS provider / Protocol"
+msgstr "SMSプロバイダ / プロトコル"
-#: settings/serializers/auth/sms.py:18 settings/serializers/auth/sms.py:36
-#: settings/serializers/auth/sms.py:44 settings/serializers/email.py:65
+#: settings/serializers/auth/sms.py:22 settings/serializers/auth/sms.py:43
+#: settings/serializers/auth/sms.py:51 settings/serializers/auth/sms.py:62
+#: settings/serializers/email.py:65
msgid "Signature"
msgstr "署名"
-#: settings/serializers/auth/sms.py:19 settings/serializers/auth/sms.py:37
-#: settings/serializers/auth/sms.py:45
+#: settings/serializers/auth/sms.py:23 settings/serializers/auth/sms.py:44
+#: settings/serializers/auth/sms.py:52
msgid "Template code"
msgstr "テンプレートコード"
-#: settings/serializers/auth/sms.py:23
+#: settings/serializers/auth/sms.py:29
msgid "Test phone"
msgstr "テスト電話"
-#: settings/serializers/auth/sso.py:12
+#: settings/serializers/auth/sms.py:58
+msgid "Enterprise code(SP id)"
+msgstr "企業コード(SP id)"
+
+#: settings/serializers/auth/sms.py:59
+msgid "Shared secret(Shared secret)"
+msgstr "パスワードを共有する(Shared secret)"
+
+#: settings/serializers/auth/sms.py:60
+msgid "Original number(Src id)"
+msgstr "元の番号(Src id)"
+
+#: settings/serializers/auth/sms.py:61
+msgid "Business type(Service id)"
+msgstr "ビジネス・タイプ(Service id)"
+
+#: settings/serializers/auth/sms.py:64
+msgid "Template"
+msgstr "テンプレート"
+
+#: settings/serializers/auth/sms.py:65
+#, python-brace-format
+msgid ""
+"Template need contain {code} and Signature + template length does not exceed "
+"67 words. For example, your verification code is {code}, which is valid for "
+"5 minutes. Please do not disclose it to others."
+msgstr ""
+"テンプレートには{code}を含める必要があり、署名+テンプレートの長さは67ワード未"
+"満です。たとえば、認証コードは{code}で、有効期間は5分です。他の人には言わない"
+"でください。"
+
+#: settings/serializers/auth/sms.py:74
+#, python-brace-format
+msgid "The template needs to contain {code}"
+msgstr "テンプレートには{code}を含める必要があります"
+
+#: settings/serializers/auth/sms.py:77
+msgid "Signature + Template must not exceed 65 words"
+msgstr "署名+テンプレートの長さは65文字以内"
+
+#: settings/serializers/auth/sso.py:11
msgid "Enable SSO auth"
msgstr "SSO Token認証の有効化"
-#: settings/serializers/auth/sso.py:13
+#: settings/serializers/auth/sso.py:12
msgid "Other service can using SSO token login to JumpServer without password"
msgstr ""
"他のサービスはパスワードなしでJumpServerへのSSOトークンログインを使用できます"
-#: settings/serializers/auth/sso.py:16
+#: settings/serializers/auth/sso.py:15
msgid "SSO auth key TTL"
msgstr "Token有効期間"
-#: settings/serializers/auth/sso.py:16
+#: settings/serializers/auth/sso.py:15
+#: xpack/plugins/cloud/serializers/account_attrs.py:159
msgid "Unit: second"
msgstr "単位: 秒"
@@ -3881,7 +3991,7 @@ msgstr "ログインログは日数を保持します"
#: settings/serializers/cleaning.py:10 settings/serializers/cleaning.py:14
#: settings/serializers/cleaning.py:18 settings/serializers/cleaning.py:22
-#: settings/serializers/cleaning.py:26
+#: settings/serializers/cleaning.py:26 settings/serializers/other.py:35
msgid "Unit: day"
msgstr "単位: 日"
@@ -4054,19 +4164,23 @@ msgstr ""
"ノードが表示されないようにしますが、そのノードが許可されていないという質問に"
"質問"
-#: settings/serializers/other.py:34
+#: settings/serializers/other.py:35
+msgid "Ticket authorize default time"
+msgstr "デフォルト製造オーダ承認時間"
+
+#: settings/serializers/other.py:39
msgid "Help Docs URL"
msgstr "ドキュメントリンク"
-#: settings/serializers/other.py:35
+#: settings/serializers/other.py:40
msgid "default: http://docs.jumpserver.org"
msgstr "デフォルト: http://docs.jumpserver.org"
-#: settings/serializers/other.py:39
+#: settings/serializers/other.py:44
msgid "Help Support URL"
msgstr "サポートリンク"
-#: settings/serializers/other.py:40
+#: settings/serializers/other.py:45
msgid "default: http://www.jumpserver.org/support/"
msgstr "デフォルト: http://www.jumpserver.org/support/"
@@ -4455,10 +4569,6 @@ msgstr "成功: {} 人のユーザーに一致"
msgid "Authentication failed (configuration incorrect): {}"
msgstr "認証に失敗しました (設定が正しくありません): {}"
-#: settings/utils/ldap.py:652
-msgid "Authentication failed (before login check failed): {}"
-msgstr "認証に失敗しました (ログインチェックが失敗する前): {}"
-
#: settings/utils/ldap.py:654
msgid "Authentication failed (username or password incorrect): {}"
msgstr "認証に失敗しました (ユーザー名またはパスワードが正しくありません): {}"
@@ -4629,10 +4739,6 @@ msgstr "確認コードが送信されました"
msgid "Home page"
msgstr "ホームページ"
-#: templates/flash_message_standalone.html:28 tickets/const.py:20
-msgid "Cancel"
-msgstr "キャンセル"
-
#: templates/resource_download.html:18 templates/resource_download.html:31
msgid "Client"
msgstr "クライアント"
@@ -4678,7 +4784,7 @@ msgstr ""
msgid "Offline video player"
msgstr "オフラインビデオプレーヤー"
-#: terminal/api/endpoint.py:26
+#: terminal/api/endpoint.py:34
msgid "Not found protocol query params"
msgstr "プロトコルクエリパラメータが見つかりません"
@@ -4764,7 +4870,7 @@ msgid "Output"
msgstr "出力"
#: terminal/backends/command/models.py:25 terminal/models/replay.py:9
-#: terminal/models/sharing.py:19 terminal/models/sharing.py:76
+#: terminal/models/sharing.py:19 terminal/models/sharing.py:78
#: terminal/templates/terminal/_msg_command_alert.html:10
#: tickets/models/ticket/command_confirm.py:20
msgid "Session"
@@ -4815,7 +4921,7 @@ msgstr "一括作成非サポート"
msgid "Storage is invalid"
msgstr "ストレージが無効です"
-#: terminal/models/command.py:53
+#: terminal/models/command.py:66
msgid "Command record"
msgstr "コマンドレコード"
@@ -4851,18 +4957,26 @@ msgstr "PostgreSQL ポート"
msgid "Redis Port"
msgstr "Redis ポート"
-#: terminal/models/endpoint.py:26 terminal/models/endpoint.py:90
-#: terminal/serializers/endpoint.py:45 terminal/serializers/storage.py:38
+#: terminal/models/endpoint.py:21
+msgid "Oracle 11g Port"
+msgstr "Oracle 11g ポート"
+
+#: terminal/models/endpoint.py:22
+msgid "Oracle 12c Port"
+msgstr "Oracle 12c ポート"
+
+#: terminal/models/endpoint.py:28 terminal/models/endpoint.py:94
+#: terminal/serializers/endpoint.py:57 terminal/serializers/storage.py:38
#: terminal/serializers/storage.py:50 terminal/serializers/storage.py:80
#: terminal/serializers/storage.py:90 terminal/serializers/storage.py:98
msgid "Endpoint"
msgstr "エンドポイント"
-#: terminal/models/endpoint.py:83
+#: terminal/models/endpoint.py:87
msgid "IP group"
msgstr "IP グループ"
-#: terminal/models/endpoint.py:95
+#: terminal/models/endpoint.py:99
msgid "Endpoint rule"
msgstr "エンドポイントルール"
@@ -4878,7 +4992,7 @@ msgstr "セッションのリプレイをアップロードできます"
msgid "Can download session replay"
msgstr "セッション再生をダウンロードできます"
-#: terminal/models/session.py:50 terminal/models/sharing.py:99
+#: terminal/models/session.py:50 terminal/models/sharing.py:101
msgid "Login from"
msgstr "ログイン元"
@@ -4914,7 +5028,7 @@ msgstr "セッションアクションのパーマを検証できます"
msgid "Creator"
msgstr "作成者"
-#: terminal/models/sharing.py:26 terminal/models/sharing.py:78
+#: terminal/models/sharing.py:26 terminal/models/sharing.py:80
msgid "Verify code"
msgstr "コードの確認"
@@ -4922,7 +5036,7 @@ msgstr "コードの確認"
msgid "Expired time (min)"
msgstr "期限切れ時間 (分)"
-#: terminal/models/sharing.py:37 terminal/models/sharing.py:81
+#: terminal/models/sharing.py:37 terminal/models/sharing.py:83
msgid "Session sharing"
msgstr "セッション共有"
@@ -4930,40 +5044,40 @@ msgstr "セッション共有"
msgid "Can add super session sharing"
msgstr "スーパーセッション共有を追加できます"
-#: terminal/models/sharing.py:64
+#: terminal/models/sharing.py:66
msgid "Link not active"
msgstr "リンクがアクティブでない"
-#: terminal/models/sharing.py:66
+#: terminal/models/sharing.py:68
msgid "Link expired"
msgstr "リンク期限切れ"
-#: terminal/models/sharing.py:68
+#: terminal/models/sharing.py:70
msgid "User not allowed to join"
-msgstr "IPは許可されていません"
+msgstr "ユーザーはセッションに参加できません"
-#: terminal/models/sharing.py:85 terminal/serializers/sharing.py:59
+#: terminal/models/sharing.py:87 terminal/serializers/sharing.py:59
msgid "Joiner"
msgstr "ジョイナー"
-#: terminal/models/sharing.py:88
+#: terminal/models/sharing.py:90
msgid "Date joined"
msgstr "参加日"
-#: terminal/models/sharing.py:91
+#: terminal/models/sharing.py:93
msgid "Date left"
msgstr "日付が残っています"
-#: terminal/models/sharing.py:109 tickets/const.py:26
+#: terminal/models/sharing.py:111 tickets/const.py:26
#: xpack/plugins/change_auth_plan/models/base.py:192
msgid "Finished"
msgstr "終了"
-#: terminal/models/sharing.py:114
+#: terminal/models/sharing.py:116
msgid "Session join record"
msgstr "セッション参加記録"
-#: terminal/models/sharing.py:130
+#: terminal/models/sharing.py:132
msgid "Invalid verification code"
msgstr "検証コードが無効"
@@ -5019,7 +5133,7 @@ msgstr "クワーグ"
msgid "type"
msgstr "タイプ"
-#: terminal/models/terminal.py:183 terminal/serializers/session.py:39
+#: terminal/models/terminal.py:183
msgid "Terminal"
msgstr "ターミナル"
@@ -5043,7 +5157,11 @@ msgstr "レベル"
msgid "Batch danger command alert"
msgstr "一括危険コマンド警告"
-#: terminal/serializers/endpoint.py:39
+#: terminal/serializers/endpoint.py:12
+msgid "Oracle port"
+msgstr ""
+
+#: terminal/serializers/endpoint.py:51
msgid ""
"If asset IP addresses under different endpoints conflict, use asset labels"
msgstr ""
@@ -5078,6 +5196,10 @@ msgstr "再生できます"
msgid "Can join"
msgstr "参加できます"
+#: terminal/serializers/session.py:39
+msgid "Terminal ID"
+msgstr "ターミナル ID"
+
#: terminal/serializers/session.py:41
msgid "Can terminate"
msgstr "終了できます"
@@ -5095,12 +5217,12 @@ msgid "Bucket"
msgstr "バケット"
#: terminal/serializers/storage.py:30
-#: xpack/plugins/cloud/serializers/account_attrs.py:15
+#: xpack/plugins/cloud/serializers/account_attrs.py:17
msgid "Access key id"
msgstr "アクセスキー"
#: terminal/serializers/storage.py:34
-#: xpack/plugins/cloud/serializers/account_attrs.py:18
+#: xpack/plugins/cloud/serializers/account_attrs.py:20
msgid "Access key secret"
msgstr "アクセスキーシークレット"
@@ -5240,7 +5362,7 @@ msgstr "カスタムユーザー"
msgid "Ticket already closed"
msgstr "チケットはすでに閉じています"
-#: tickets/handlers/apply_application.py:35
+#: tickets/handlers/apply_application.py:38
msgid ""
"Created by the ticket, ticket title: {}, ticket applicant: {}, ticket "
"processor: {}, ticket ID: {}"
@@ -5248,7 +5370,7 @@ msgstr ""
"チケットによって作成されたチケットタイトル: {}、チケット申請者: {}、チケット"
"処理者: {}、チケットID: {}"
-#: tickets/handlers/apply_asset.py:35
+#: tickets/handlers/apply_asset.py:37
msgid ""
"Created by the ticket ticket title: {} ticket applicant: {} ticket "
"processor: {} ticket ID: {}"
@@ -5256,19 +5378,19 @@ msgstr ""
"チケットのタイトル: {} チケット申請者: {} チケットプロセッサ: {} チケットID: "
"{}"
-#: tickets/handlers/base.py:79
+#: tickets/handlers/base.py:84
msgid "Change field"
msgstr "フィールドを変更"
-#: tickets/handlers/base.py:79
+#: tickets/handlers/base.py:84
msgid "Before change"
msgstr "変更前"
-#: tickets/handlers/base.py:79
+#: tickets/handlers/base.py:84
msgid "After change"
msgstr "変更後"
-#: tickets/handlers/base.py:91
+#: tickets/handlers/base.py:96
msgid "{} {} the ticket"
msgstr "{} {} チケット"
@@ -5306,11 +5428,11 @@ msgstr "ボディ"
msgid "Approve level"
msgstr "レベルを承認する"
-#: tickets/models/flow.py:25 tickets/serializers/flow.py:14
+#: tickets/models/flow.py:25 tickets/serializers/flow.py:15
msgid "Approve strategy"
msgstr "戦略を承認する"
-#: tickets/models/flow.py:30 tickets/serializers/flow.py:15
+#: tickets/models/flow.py:30 tickets/serializers/flow.py:16
msgid "Assignees"
msgstr "アシニーズ"
@@ -5326,16 +5448,16 @@ msgstr "チケットの流れ"
msgid "Ticket session relation"
msgstr "チケットセッションの関係"
-#: tickets/models/ticket/apply_application.py:11
+#: tickets/models/ticket/apply_application.py:12
#: tickets/models/ticket/apply_asset.py:13
msgid "Permission name"
msgstr "認可ルール名"
-#: tickets/models/ticket/apply_application.py:20
+#: tickets/models/ticket/apply_application.py:21
msgid "Apply applications"
msgstr "アプリケーションの適用"
-#: tickets/models/ticket/apply_application.py:23
+#: tickets/models/ticket/apply_application.py:24
#: tickets/models/ticket/apply_asset.py:18
msgid "Apply system users"
msgstr "システムユーザーの適用"
@@ -5453,15 +5575,15 @@ msgstr "チケットが処理されました。プロセッサー- {}"
msgid "Ticket has processed - {} ({})"
msgstr "チケットが処理済み- {} ({})"
-#: tickets/serializers/flow.py:16
+#: tickets/serializers/flow.py:17
msgid "Assignees display"
msgstr "受付者名"
-#: tickets/serializers/flow.py:42
+#: tickets/serializers/flow.py:43
msgid "Please select the Assignees"
msgstr "受付をお選びください"
-#: tickets/serializers/flow.py:68
+#: tickets/serializers/flow.py:69
msgid "The current organization type already exists"
msgstr "現在の組織タイプは既に存在します。"
@@ -5482,7 +5604,7 @@ msgstr "有効期限は開始日より大きくする必要があります"
msgid "Permission named `{}` already exists"
msgstr "'{}'という名前の権限は既に存在します"
-#: tickets/serializers/ticket/ticket.py:92
+#: tickets/serializers/ticket/ticket.py:99
msgid "The ticket flow `{}` does not exist"
msgstr "チケットフロー '{}'が存在しない"
@@ -5641,7 +5763,7 @@ msgstr "公開鍵は古いものと同じであってはなりません。"
msgid "Not a valid ssh public key"
msgstr "有効なssh公開鍵ではありません"
-#: users/forms/profile.py:161 users/models/user.py:692
+#: users/forms/profile.py:161 users/models/user.py:696
msgid "Public key"
msgstr "公開キー"
@@ -5653,55 +5775,55 @@ msgstr "強制有効"
msgid "Local"
msgstr "ローカル"
-#: users/models/user.py:673 users/serializers/user.py:149
+#: users/models/user.py:677 users/serializers/user.py:149
msgid "Is service account"
msgstr "サービスアカウントです"
-#: users/models/user.py:675
+#: users/models/user.py:679
msgid "Avatar"
msgstr "アバター"
-#: users/models/user.py:678
+#: users/models/user.py:682
msgid "Wechat"
msgstr "微信"
-#: users/models/user.py:695
+#: users/models/user.py:699
msgid "Secret key"
msgstr "秘密キー"
-#: users/models/user.py:711
+#: users/models/user.py:715
msgid "Source"
msgstr "ソース"
-#: users/models/user.py:715
+#: users/models/user.py:719
msgid "Date password last updated"
msgstr "最終更新日パスワード"
-#: users/models/user.py:718
+#: users/models/user.py:722
msgid "Need update password"
msgstr "更新パスワードが必要"
-#: users/models/user.py:892
+#: users/models/user.py:896
msgid "Can invite user"
msgstr "ユーザーを招待できます"
-#: users/models/user.py:893
+#: users/models/user.py:897
msgid "Can remove user"
msgstr "ユーザーを削除できます"
-#: users/models/user.py:894
+#: users/models/user.py:898
msgid "Can match user"
msgstr "ユーザーに一致できます"
-#: users/models/user.py:903
+#: users/models/user.py:907
msgid "Administrator"
msgstr "管理者"
-#: users/models/user.py:906
+#: users/models/user.py:910
msgid "Administrator is the super user of system"
msgstr "管理者はシステムのスーパーユーザーです"
-#: users/models/user.py:931
+#: users/models/user.py:935
msgid "User password history"
msgstr "ユーザーパスワード履歴"
@@ -5908,11 +6030,11 @@ msgstr "あなたのssh公開鍵はサイト管理者によってリセットさ
msgid "click here to set your password"
msgstr "ここをクリックしてパスワードを設定してください"
-#: users/templates/users/forgot_password.html:23
+#: users/templates/users/forgot_password.html:24
msgid "Input your email, that will send a mail to your"
msgstr "あなたのメールを入力し、それはあなたにメールを送信します"
-#: users/templates/users/forgot_password.html:32
+#: users/templates/users/forgot_password.html:33
msgid "Submit"
msgstr "送信"
@@ -6355,31 +6477,31 @@ msgstr "谷歌雲"
msgid "Fusion Compute"
msgstr ""
-#: xpack/plugins/cloud/const.py:27
+#: xpack/plugins/cloud/const.py:28
msgid "Instance name"
msgstr "インスタンス名"
-#: xpack/plugins/cloud/const.py:28
+#: xpack/plugins/cloud/const.py:29
msgid "Instance name and Partial IP"
msgstr "インスタンス名と部分IP"
-#: xpack/plugins/cloud/const.py:33
+#: xpack/plugins/cloud/const.py:34
msgid "Succeed"
msgstr "成功"
-#: xpack/plugins/cloud/const.py:37
+#: xpack/plugins/cloud/const.py:38
msgid "Unsync"
msgstr "同期していません"
-#: xpack/plugins/cloud/const.py:38
+#: xpack/plugins/cloud/const.py:39
msgid "New Sync"
msgstr "新しい同期"
-#: xpack/plugins/cloud/const.py:39
+#: xpack/plugins/cloud/const.py:40
msgid "Synced"
msgstr "同期済み"
-#: xpack/plugins/cloud/const.py:40
+#: xpack/plugins/cloud/const.py:41
msgid "Released"
msgstr "リリース済み"
@@ -6649,52 +6771,88 @@ msgstr "華南-広州-友好ユーザー環境"
msgid "CN East-Suqian"
msgstr "華東-宿遷"
-#: xpack/plugins/cloud/serializers/account.py:61
+#: xpack/plugins/cloud/serializers/account.py:62
msgid "Validity display"
msgstr "有効表示"
-#: xpack/plugins/cloud/serializers/account.py:62
+#: xpack/plugins/cloud/serializers/account.py:63
msgid "Provider display"
msgstr "プロバイダ表示"
-#: xpack/plugins/cloud/serializers/account_attrs.py:33
+#: xpack/plugins/cloud/serializers/account_attrs.py:35
msgid "Client ID"
msgstr "クライアントID"
-#: xpack/plugins/cloud/serializers/account_attrs.py:39
+#: xpack/plugins/cloud/serializers/account_attrs.py:41
msgid "Tenant ID"
msgstr "テナントID"
-#: xpack/plugins/cloud/serializers/account_attrs.py:42
+#: xpack/plugins/cloud/serializers/account_attrs.py:44
msgid "Subscription ID"
msgstr "サブスクリプションID"
-#: xpack/plugins/cloud/serializers/account_attrs.py:93
-#: xpack/plugins/cloud/serializers/account_attrs.py:98
-#: xpack/plugins/cloud/serializers/account_attrs.py:122
+#: xpack/plugins/cloud/serializers/account_attrs.py:95
+#: xpack/plugins/cloud/serializers/account_attrs.py:100
+#: xpack/plugins/cloud/serializers/account_attrs.py:124
msgid "API Endpoint"
msgstr "APIエンドポイント"
-#: xpack/plugins/cloud/serializers/account_attrs.py:104
+#: xpack/plugins/cloud/serializers/account_attrs.py:106
msgid "Auth url"
msgstr "認証アドレス"
-#: xpack/plugins/cloud/serializers/account_attrs.py:105
+#: xpack/plugins/cloud/serializers/account_attrs.py:107
msgid "eg: http://openstack.example.com:5000/v3"
msgstr "例えば: http://openstack.example.com:5000/v3"
-#: xpack/plugins/cloud/serializers/account_attrs.py:108
+#: xpack/plugins/cloud/serializers/account_attrs.py:110
msgid "User domain"
msgstr "ユーザードメイン"
-#: xpack/plugins/cloud/serializers/account_attrs.py:115
+#: xpack/plugins/cloud/serializers/account_attrs.py:117
msgid "Service account key"
msgstr "サービスアカウントキー"
-#: xpack/plugins/cloud/serializers/account_attrs.py:116
+#: xpack/plugins/cloud/serializers/account_attrs.py:118
msgid "The file is in JSON format"
msgstr "ファイルはJSON形式です。"
+#: xpack/plugins/cloud/serializers/account_attrs.py:131
+msgid "IP address invalid `{}`, {}"
+msgstr "IPアドレスが無効: '{}', {}"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:137
+msgid ""
+"Format for comma-delimited string,Such as: 192.168.1.0/24, "
+"10.0.0.0-10.0.0.255"
+msgstr "形式はコンマ区切りの文字列です,例:192.168.1.0/24,10.0.0.0-10.0.0.255"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:141
+msgid ""
+"The port is used to detect the validity of the IP address. When the "
+"synchronization task is executed, only the valid IP address will be "
+"synchronized.
If the port is 0, all IP addresses are valid."
+msgstr ""
+"このポートは、 IP アドレスの有効性を検出するために使用されます。同期タスクが"
+"実行されると、有効な IP アドレスのみが同期されます。
ポートが0の場合、す"
+"べてのIPアドレスが有効です。"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:149
+msgid "Hostname prefix"
+msgstr "ホスト名プレフィックス"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:152
+msgid "IP segment"
+msgstr "IP セグメント"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:156
+msgid "Test port"
+msgstr "テストポート"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:159
+msgid "Test timeout"
+msgstr "テストタイムアウト"
+
#: xpack/plugins/cloud/serializers/task.py:29
msgid ""
"Only instances matching the IP range will be synced.
If the instance "
@@ -6703,12 +6861,11 @@ msgid ""
"all instances and randomly match IP addresses.
Format for comma-"
"delimited string, Such as: 192.168.1.0/24, 10.1.1.1-10.1.1.20"
msgstr ""
-"IP範囲に一致するインスタンスのみが同期されます。
"
-"インスタンスに複数のIPアドレスが含まれている場合、一致する最初のIPアドレスが作成されたアセットのIPとして使用されます。
"
-"デフォルト値の*は、すべてのインスタンスを同期し、IPアドレスをランダムに一致させることを意味します。
"
-"形式はコンマ区切りの文字列です。例:192.168.1.0/24,10.1.1.1-10.1.1.20"
-
-
+"IP範囲に一致するインスタンスのみが同期されます。
インスタンスに複数のIPア"
+"ドレスが含まれている場合、一致する最初のIPアドレスが作成されたアセットのIPと"
+"して使用されます。
デフォルト値の*は、すべてのインスタンスを同期し、IPア"
+"ドレスをランダムに一致させることを意味します。
形式はコンマ区切りの文字列"
+"です。例:192.168.1.0/24,10.1.1.1-10.1.1.20"
#: xpack/plugins/cloud/serializers/task.py:36
msgid "History count"
@@ -6814,6 +6971,3 @@ msgstr "究極のエディション"
#: xpack/plugins/license/models.py:77
msgid "Community edition"
msgstr "コミュニティ版"
-
-#~ msgid "User cannot self-update fields: {}"
-#~ msgstr "ユーザーは自分のフィールドを更新できません: {}"
diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo
index 6d69faf53..d675a6b92 100644
--- a/apps/locale/zh/LC_MESSAGES/django.mo
+++ b/apps/locale/zh/LC_MESSAGES/django.mo
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:54c9c54a2e5ae5d27eb79f8ce0d19e7f362c016efb8c6011cace7bd2cb7eec1c
-size 108123
+oid sha256:c6f584a0c74107ceddce6b403ff8755b59aabb093a0e6cc0c5f9b47eb6ae49f4
+size 255
diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po
index 892d7cb6f..7a9a812ff 100644
--- a/apps/locale/zh/LC_MESSAGES/django.po
+++ b/apps/locale/zh/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: JumpServer 0.3.3\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-07-12 17:58+0800\n"
+"POT-Creation-Date: 2022-08-17 16:28+0800\n"
"PO-Revision-Date: 2021-05-20 10:54+0800\n"
"Last-Translator: ibuler \n"
"Language-Team: JumpServer team\n"
@@ -22,27 +22,27 @@ msgid "Acls"
msgstr "访问控制"
#: acls/models/base.py:25 acls/serializers/login_asset_acl.py:47
-#: applications/models/application.py:217 assets/models/asset.py:138
+#: applications/models/application.py:220 assets/models/asset.py:138
#: assets/models/base.py:175 assets/models/cluster.py:18
#: assets/models/cmd_filter.py:27 assets/models/domain.py:23
#: assets/models/group.py:20 assets/models/label.py:18 ops/mixin.py:24
-#: orgs/models.py:65 perms/models/base.py:83 rbac/models/role.py:29
-#: settings/models.py:29 settings/serializers/sms.py:6
-#: terminal/models/endpoint.py:10 terminal/models/endpoint.py:82
+#: orgs/models.py:70 perms/models/base.py:83 rbac/models/role.py:29
+#: settings/models.py:33 settings/serializers/sms.py:6
+#: terminal/models/endpoint.py:10 terminal/models/endpoint.py:86
#: terminal/models/storage.py:26 terminal/models/task.py:16
#: terminal/models/terminal.py:100 users/forms/profile.py:33
-#: users/models/group.py:15 users/models/user.py:661
+#: users/models/group.py:15 users/models/user.py:665
#: xpack/plugins/cloud/models.py:28
msgid "Name"
msgstr "名称"
#: acls/models/base.py:27 assets/models/cmd_filter.py:84
-#: assets/models/user.py:251 terminal/models/endpoint.py:85
+#: assets/models/user.py:251 terminal/models/endpoint.py:89
msgid "Priority"
msgstr "优先级"
#: acls/models/base.py:28 assets/models/cmd_filter.py:84
-#: assets/models/user.py:251 terminal/models/endpoint.py:86
+#: assets/models/user.py:251 terminal/models/endpoint.py:90
msgid "1-100, the lower the value will be match first"
msgstr "优先级可选范围为 1-100 (数值越小越优先)"
@@ -52,18 +52,18 @@ msgstr "优先级可选范围为 1-100 (数值越小越优先)"
msgid "Active"
msgstr "激活中"
-#: acls/models/base.py:32 applications/models/application.py:230
+#: acls/models/base.py:32 applications/models/application.py:233
#: assets/models/asset.py:143 assets/models/asset.py:231
#: assets/models/backup.py:54 assets/models/base.py:180
#: assets/models/cluster.py:29 assets/models/cmd_filter.py:48
#: assets/models/cmd_filter.py:96 assets/models/domain.py:24
#: assets/models/domain.py:65 assets/models/group.py:23
-#: assets/models/label.py:23 ops/models/adhoc.py:38 orgs/models.py:68
-#: perms/models/base.py:93 rbac/models/role.py:37 settings/models.py:34
-#: terminal/models/endpoint.py:21 terminal/models/endpoint.py:92
+#: assets/models/label.py:23 ops/models/adhoc.py:38 orgs/models.py:73
+#: perms/models/base.py:93 rbac/models/role.py:37 settings/models.py:38
+#: terminal/models/endpoint.py:23 terminal/models/endpoint.py:96
#: terminal/models/storage.py:29 terminal/models/terminal.py:114
#: tickets/models/comment.py:32 tickets/models/ticket/general.py:288
-#: users/models/group.py:16 users/models/user.py:698
+#: users/models/group.py:16 users/models/user.py:702
#: xpack/plugins/change_auth_plan/models/base.py:44
#: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:116
#: xpack/plugins/gathered_user/models.py:26
@@ -79,7 +79,7 @@ msgstr "拒绝"
msgid "Allow"
msgstr "允许"
-#: acls/models/login_acl.py:20 acls/models/login_acl.py:104
+#: acls/models/login_acl.py:20 acls/models/login_acl.py:75
#: acls/models/login_asset_acl.py:17 tickets/const.py:9
msgid "Login confirm"
msgstr "登录复核"
@@ -87,13 +87,13 @@ msgstr "登录复核"
#: acls/models/login_acl.py:24 acls/models/login_asset_acl.py:20
#: assets/models/cmd_filter.py:30 assets/models/label.py:15 audits/models.py:37
#: audits/models.py:62 audits/models.py:87 audits/serializers.py:100
-#: authentication/models.py:54 authentication/models.py:78 orgs/models.py:214
-#: perms/models/base.py:84 rbac/builtin.py:118 rbac/models/rolebinding.py:41
+#: authentication/models.py:54 authentication/models.py:78 orgs/models.py:220
+#: perms/models/base.py:84 rbac/builtin.py:120 rbac/models/rolebinding.py:41
#: terminal/backends/command/models.py:20
#: terminal/backends/command/serializers.py:13 terminal/models/session.py:44
#: terminal/models/sharing.py:33 terminal/notifications.py:91
#: terminal/notifications.py:139 tickets/models/comment.py:21 users/const.py:14
-#: users/models/user.py:890 users/models/user.py:921
+#: users/models/user.py:894 users/models/user.py:925
#: users/serializers/group.py:19
msgid "User"
msgstr "用户"
@@ -156,14 +156,14 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. "
#: acls/serializers/login_asset_acl.py:51 assets/models/base.py:176
#: assets/models/gathered_user.py:15 audits/models.py:121
#: authentication/forms.py:25 authentication/forms.py:27
-#: authentication/models.py:245
+#: authentication/models.py:260
#: authentication/templates/authentication/_msg_different_city.html:9
#: authentication/templates/authentication/_msg_oauth_bind.html:9
-#: ops/models/adhoc.py:159 users/forms/profile.py:32 users/models/user.py:659
+#: ops/models/adhoc.py:159 users/forms/profile.py:32 users/models/user.py:663
#: users/templates/users/_msg_user_created.html:12
#: xpack/plugins/change_auth_plan/models/asset.py:34
#: xpack/plugins/change_auth_plan/models/asset.py:195
-#: xpack/plugins/cloud/serializers/account_attrs.py:24
+#: xpack/plugins/cloud/serializers/account_attrs.py:26
msgid "Username"
msgstr "用户名"
@@ -183,7 +183,7 @@ msgstr ""
#: authentication/templates/authentication/_msg_oauth_bind.html:12
#: authentication/templates/authentication/_msg_rest_password_success.html:8
#: authentication/templates/authentication/_msg_rest_public_key_success.html:8
-#: settings/serializers/terminal.py:8 terminal/serializers/endpoint.py:42
+#: settings/serializers/terminal.py:8 terminal/serializers/endpoint.py:54
msgid "IP"
msgstr "IP"
@@ -210,7 +210,7 @@ msgid "Unsupported protocols: {}"
msgstr "不支持的协议: {}"
#: acls/serializers/login_asset_acl.py:98
-#: tickets/serializers/ticket/ticket.py:78
+#: tickets/serializers/ticket/ticket.py:85
msgid "The organization `{}` does not exist"
msgstr "组织 `{}` 不存在"
@@ -236,7 +236,7 @@ msgstr ""
msgid "Time Period"
msgstr "时段"
-#: applications/apps.py:9 applications/models/application.py:63
+#: applications/apps.py:9 applications/models/application.py:64
msgid "Applications"
msgstr "应用管理"
@@ -255,7 +255,11 @@ msgstr "远程应用"
msgid "Custom"
msgstr "自定义"
-#: applications/models/account.py:12 applications/models/application.py:234
+#: applications/const.py:91 rbac/tree.py:29
+msgid "Other"
+msgstr "其它"
+
+#: applications/models/account.py:12 applications/models/application.py:237
#: assets/models/backup.py:32 assets/models/cmd_filter.py:45
#: authentication/models.py:67 authentication/models.py:95
#: perms/models/application_permission.py:28
@@ -273,8 +277,9 @@ msgstr "应用程序"
msgid "System user"
msgstr "系统用户"
-#: applications/models/account.py:17 assets/models/authbook.py:21
-#: settings/serializers/auth/cas.py:18
+#: applications/models/account.py:17
+#: applications/serializers/attrs/application_type/oracle.py:13
+#: assets/models/authbook.py:21 settings/serializers/auth/cas.py:18
msgid "Version"
msgstr "版本"
@@ -290,44 +295,44 @@ msgstr "可以查看应用账号密码"
msgid "Can change application account secret"
msgstr "可以查看应用账号密码"
-#: applications/models/application.py:219
+#: applications/models/application.py:222
#: applications/serializers/application.py:99 assets/models/label.py:21
#: perms/models/application_permission.py:21
#: perms/serializers/application/user_permission.py:33
-#: tickets/models/ticket/apply_application.py:14
+#: tickets/models/ticket/apply_application.py:15
#: xpack/plugins/change_auth_plan/models/app.py:25
msgid "Category"
msgstr "类别"
-#: applications/models/application.py:222
+#: applications/models/application.py:225
#: applications/serializers/application.py:101 assets/models/backup.py:49
#: assets/models/cmd_filter.py:82 assets/models/user.py:250
#: authentication/models.py:70 perms/models/application_permission.py:24
#: perms/serializers/application/user_permission.py:34
#: terminal/models/storage.py:58 terminal/models/storage.py:143
#: tickets/models/comment.py:26 tickets/models/flow.py:57
-#: tickets/models/ticket/apply_application.py:17
+#: tickets/models/ticket/apply_application.py:18
#: tickets/models/ticket/general.py:273
#: xpack/plugins/change_auth_plan/models/app.py:28
#: xpack/plugins/change_auth_plan/models/app.py:153
msgid "Type"
msgstr "类型"
-#: applications/models/application.py:226 assets/models/asset.py:217
+#: applications/models/application.py:229 assets/models/asset.py:217
#: assets/models/domain.py:29 assets/models/domain.py:64
msgid "Domain"
msgstr "网域"
-#: applications/models/application.py:228 xpack/plugins/cloud/models.py:33
-#: xpack/plugins/cloud/serializers/account.py:60
+#: applications/models/application.py:231 xpack/plugins/cloud/models.py:33
+#: xpack/plugins/cloud/serializers/account.py:61
msgid "Attrs"
msgstr "属性"
-#: applications/models/application.py:238
+#: applications/models/application.py:241
msgid "Can match application"
msgstr "匹配应用"
-#: applications/models/application.py:305
+#: applications/models/application.py:320
msgid "Application user"
msgstr "应用用户"
@@ -342,7 +347,7 @@ msgstr "类别名称"
#: assets/serializers/cmd_filter.py:34 assets/serializers/system_user.py:34
#: audits/serializers.py:29 authentication/serializers/connection_token.py:22
#: perms/serializers/application/permission.py:19
-#: tickets/serializers/flow.py:48 tickets/serializers/ticket/ticket.py:17
+#: tickets/serializers/flow.py:49 tickets/serializers/ticket/ticket.py:17
msgid "Type display"
msgstr "类型名称"
@@ -353,8 +358,8 @@ msgstr "类型名称"
#: assets/serializers/account.py:18 assets/serializers/cmd_filter.py:28
#: assets/serializers/cmd_filter.py:48 common/db/models.py:114
#: common/mixins/models.py:50 ops/models/adhoc.py:39 ops/models/command.py:30
-#: orgs/models.py:67 orgs/models.py:217 perms/models/base.py:92
-#: users/models/group.py:18 users/models/user.py:922
+#: orgs/models.py:72 orgs/models.py:223 perms/models/base.py:92
+#: users/models/group.py:18 users/models/user.py:926
#: xpack/plugins/cloud/models.py:125
msgid "Date created"
msgstr "创建日期"
@@ -363,7 +368,7 @@ msgstr "创建日期"
#: assets/models/gathered_user.py:20 assets/serializers/account.py:21
#: assets/serializers/cmd_filter.py:29 assets/serializers/cmd_filter.py:49
#: common/db/models.py:115 common/mixins/models.py:51 ops/models/adhoc.py:40
-#: orgs/models.py:218
+#: orgs/models.py:224
msgid "Date updated"
msgstr "更新日期"
@@ -383,8 +388,8 @@ msgstr "集群"
#: applications/serializers/attrs/application_category/db.py:11
#: ops/models/adhoc.py:157 settings/serializers/auth/radius.py:14
-#: terminal/models/endpoint.py:11
-#: xpack/plugins/cloud/serializers/account_attrs.py:70
+#: settings/serializers/auth/sms.py:56 terminal/models/endpoint.py:11
+#: xpack/plugins/cloud/serializers/account_attrs.py:72
msgid "Host"
msgstr "主机"
@@ -392,16 +397,20 @@ msgstr "主机"
#: applications/serializers/attrs/application_type/mongodb.py:10
#: applications/serializers/attrs/application_type/mysql.py:10
#: applications/serializers/attrs/application_type/mysql_workbench.py:22
-#: applications/serializers/attrs/application_type/oracle.py:10
+#: applications/serializers/attrs/application_type/oracle.py:16
#: applications/serializers/attrs/application_type/pgsql.py:10
#: applications/serializers/attrs/application_type/redis.py:10
#: applications/serializers/attrs/application_type/sqlserver.py:10
#: assets/models/asset.py:214 assets/models/domain.py:62
-#: settings/serializers/auth/radius.py:15
-#: xpack/plugins/cloud/serializers/account_attrs.py:71
+#: settings/serializers/auth/radius.py:15 settings/serializers/auth/sms.py:57
+#: xpack/plugins/cloud/serializers/account_attrs.py:73
msgid "Port"
msgstr "端口"
+#: applications/serializers/attrs/application_category/remote_app.py:34
+msgid "Asset Info"
+msgstr "资产信息"
+
#: applications/serializers/attrs/application_category/remote_app.py:39
#: applications/serializers/attrs/application_type/chrome.py:14
#: applications/serializers/attrs/application_type/mysql_workbench.py:14
@@ -411,13 +420,13 @@ msgstr "应用路径"
#: applications/serializers/attrs/application_category/remote_app.py:44
#: assets/serializers/system_user.py:167
-#: tickets/serializers/ticket/apply_application.py:35
+#: tickets/serializers/ticket/apply_application.py:38
#: tickets/serializers/ticket/common.py:59
#: xpack/plugins/change_auth_plan/serializers/asset.py:67
#: xpack/plugins/change_auth_plan/serializers/asset.py:70
#: xpack/plugins/change_auth_plan/serializers/asset.py:73
#: xpack/plugins/change_auth_plan/serializers/asset.py:104
-#: xpack/plugins/cloud/serializers/account_attrs.py:54
+#: xpack/plugins/cloud/serializers/account_attrs.py:56
msgid "This field is required."
msgstr "该字段是必填项。"
@@ -462,6 +471,10 @@ msgstr "Mysql 工作台 用户名"
msgid "Mysql workbench password"
msgstr "Mysql 工作台 密码"
+#: applications/serializers/attrs/application_type/oracle.py:14
+msgid "Magnus currently supports only 11g and 12c connections"
+msgstr "目前 Magnus 只支持连接 11g、12c 版本"
+
#: applications/serializers/attrs/application_type/vmware_client.py:26
msgid "Vmware username"
msgstr "Vmware 用户名"
@@ -511,6 +524,7 @@ msgstr "内部的"
#: assets/models/asset.py:162 assets/models/asset.py:216
#: assets/serializers/account.py:15 assets/serializers/asset.py:63
#: perms/serializers/asset/user_permission.py:43
+#: xpack/plugins/cloud/serializers/account_attrs.py:162
msgid "Platform"
msgstr "系统平台"
@@ -609,8 +623,8 @@ msgstr "标签管理"
#: assets/models/asset.py:229 assets/models/base.py:183
#: assets/models/cluster.py:28 assets/models/cmd_filter.py:52
#: assets/models/cmd_filter.py:99 assets/models/group.py:21
-#: common/db/models.py:112 common/mixins/models.py:49 orgs/models.py:66
-#: orgs/models.py:219 perms/models/base.py:91 users/models/user.py:706
+#: common/db/models.py:112 common/mixins/models.py:49 orgs/models.py:71
+#: orgs/models.py:225 perms/models/base.py:91 users/models/user.py:710
#: users/serializers/group.py:33
#: xpack/plugins/change_auth_plan/models/base.py:48
#: xpack/plugins/cloud/models.py:122 xpack/plugins/gathered_user/models.py:30
@@ -693,7 +707,7 @@ msgstr "定时触发"
#: assets/models/backup.py:105 audits/models.py:44 ops/models/command.py:31
#: perms/models/base.py:89 terminal/models/session.py:58
-#: tickets/models/ticket/apply_application.py:25
+#: tickets/models/ticket/apply_application.py:29
#: tickets/models/ticket/apply_asset.py:23
#: xpack/plugins/change_auth_plan/models/base.py:112
#: xpack/plugins/change_auth_plan/models/base.py:203
@@ -721,7 +735,7 @@ msgid "Trigger mode"
msgstr "触发模式"
#: assets/models/backup.py:119 audits/models.py:127
-#: terminal/models/sharing.py:106
+#: terminal/models/sharing.py:108
#: xpack/plugins/change_auth_plan/models/base.py:201
#: xpack/plugins/change_auth_plan/serializers/app.py:66
#: xpack/plugins/change_auth_plan/serializers/asset.py:180
@@ -755,7 +769,7 @@ msgstr "成功"
#: assets/models/base.py:32 audits/models.py:118
#: xpack/plugins/change_auth_plan/serializers/app.py:88
#: xpack/plugins/change_auth_plan/serializers/asset.py:199
-#: xpack/plugins/cloud/const.py:32
+#: xpack/plugins/cloud/const.py:33
msgid "Failed"
msgstr "失败"
@@ -763,7 +777,7 @@ msgstr "失败"
msgid "Connectivity"
msgstr "可连接性"
-#: assets/models/base.py:40 authentication/models.py:248
+#: assets/models/base.py:40 authentication/models.py:263
msgid "Date verified"
msgstr "校验日期"
@@ -781,7 +795,7 @@ msgstr "校验日期"
#: xpack/plugins/change_auth_plan/models/base.py:196
#: xpack/plugins/change_auth_plan/serializers/base.py:21
#: xpack/plugins/change_auth_plan/serializers/base.py:73
-#: xpack/plugins/cloud/serializers/account_attrs.py:26
+#: xpack/plugins/cloud/serializers/account_attrs.py:28
msgid "Password"
msgstr "密码"
@@ -806,7 +820,7 @@ msgstr "带宽"
msgid "Contact"
msgstr "联系人"
-#: assets/models/cluster.py:22 users/models/user.py:681
+#: assets/models/cluster.py:22 users/models/user.py:685
msgid "Phone"
msgstr "手机"
@@ -832,7 +846,7 @@ msgid "Default"
msgstr "默认"
#: assets/models/cluster.py:36 assets/models/label.py:14 rbac/const.py:6
-#: users/models/user.py:907
+#: users/models/user.py:911
msgid "System"
msgstr "系统"
@@ -841,7 +855,7 @@ msgid "Default Cluster"
msgstr "默认Cluster"
#: assets/models/cmd_filter.py:34 perms/models/base.py:86
-#: users/models/group.py:31 users/models/user.py:667
+#: users/models/group.py:31 users/models/user.py:671
msgid "User group"
msgstr "用户组"
@@ -889,11 +903,11 @@ msgstr "忽略大小写"
msgid "Command filter rule"
msgstr "命令过滤规则"
-#: assets/models/cmd_filter.py:144
+#: assets/models/cmd_filter.py:147
msgid "The generated regular expression is incorrect: {}"
msgstr "生成的正则表达式有误"
-#: assets/models/cmd_filter.py:170 tickets/const.py:13
+#: assets/models/cmd_filter.py:173 tickets/const.py:13
msgid "Command confirm"
msgstr "命令复核"
@@ -910,7 +924,8 @@ msgstr "测试网关"
msgid "Unable to connect to port {port} on {ip}"
msgstr "无法连接到 {ip} 上的端口 {port}"
-#: assets/models/domain.py:134 xpack/plugins/cloud/providers/fc.py:48
+#: assets/models/domain.py:134 authentication/middleware.py:75
+#: xpack/plugins/cloud/providers/fc.py:48
msgid "Authentication failed"
msgstr "认证失败"
@@ -942,7 +957,7 @@ msgstr "资产组"
msgid "Default asset group"
msgstr "默认资产组"
-#: assets/models/label.py:19 assets/models/node.py:546 settings/models.py:30
+#: assets/models/label.py:19 assets/models/node.py:553 settings/models.py:34
msgid "Value"
msgstr "值"
@@ -950,32 +965,32 @@ msgstr "值"
msgid "Label"
msgstr "标签"
-#: assets/models/node.py:151
+#: assets/models/node.py:158
msgid "New node"
msgstr "新节点"
-#: assets/models/node.py:474
+#: assets/models/node.py:481
msgid "empty"
msgstr "空"
-#: assets/models/node.py:545 perms/models/asset_permission.py:101
+#: assets/models/node.py:552 perms/models/asset_permission.py:101
msgid "Key"
msgstr "键"
-#: assets/models/node.py:547 assets/serializers/node.py:20
+#: assets/models/node.py:554 assets/serializers/node.py:20
msgid "Full value"
msgstr "全称"
-#: assets/models/node.py:550 perms/models/asset_permission.py:102
+#: assets/models/node.py:557 perms/models/asset_permission.py:102
msgid "Parent key"
msgstr "ssh私钥"
-#: assets/models/node.py:559 assets/serializers/system_user.py:267
+#: assets/models/node.py:566 assets/serializers/system_user.py:267
#: xpack/plugins/cloud/models.py:96 xpack/plugins/cloud/serializers/task.py:70
msgid "Node"
msgstr "节点"
-#: assets/models/node.py:562
+#: assets/models/node.py:569
msgid "Can match node"
msgstr "可以匹配节点"
@@ -1120,6 +1135,7 @@ msgstr "CPU信息"
#: perms/serializers/application/permission.py:42
#: perms/serializers/asset/permission.py:18
#: perms/serializers/asset/permission.py:46
+#: tickets/models/ticket/apply_application.py:27
#: tickets/models/ticket/apply_asset.py:21
msgid "Actions"
msgstr "动作"
@@ -1135,7 +1151,7 @@ msgstr "定时执行"
msgid "Currently only mail sending is supported"
msgstr "当前只支持邮件发送"
-#: assets/serializers/base.py:16 users/models/user.py:689
+#: assets/serializers/base.py:16 users/models/user.py:693
msgid "Private key"
msgstr "ssh私钥"
@@ -1414,7 +1430,7 @@ msgid "Symlink"
msgstr "建立软链接"
#: audits/models.py:38 audits/models.py:66 audits/models.py:89
-#: terminal/models/session.py:51 terminal/models/sharing.py:94
+#: terminal/models/session.py:51 terminal/models/sharing.py:96
msgid "Remote addr"
msgstr "远端地址"
@@ -1426,7 +1442,7 @@ msgstr "操作"
msgid "Filename"
msgstr "文件名"
-#: audits/models.py:43 audits/models.py:117 terminal/models/sharing.py:102
+#: audits/models.py:43 audits/models.py:117 terminal/models/sharing.py:104
#: tickets/views/approve.py:115
#: xpack/plugins/change_auth_plan/serializers/app.py:87
#: xpack/plugins/change_auth_plan/serializers/asset.py:198
@@ -1481,7 +1497,7 @@ msgstr "改密日志"
msgid "Disabled"
msgstr "禁用"
-#: audits/models.py:112 settings/models.py:33
+#: audits/models.py:112 settings/models.py:37
msgid "Enabled"
msgstr "启用"
@@ -1509,7 +1525,7 @@ msgstr "用户代理"
#: audits/models.py:126
#: authentication/templates/authentication/_mfa_confirm_modal.html:14
-#: users/forms/profile.py:65 users/models/user.py:684
+#: users/forms/profile.py:65 users/models/user.py:688
#: users/serializers/profile.py:126
msgid "MFA"
msgstr "MFA"
@@ -1587,24 +1603,24 @@ msgid "Auth Token"
msgstr "认证令牌"
#: audits/signal_handlers.py:53 authentication/notifications.py:73
-#: authentication/views/login.py:67 authentication/views/wecom.py:178
-#: notifications/backends/__init__.py:11 users/models/user.py:720
+#: authentication/views/login.py:73 authentication/views/wecom.py:178
+#: notifications/backends/__init__.py:11 users/models/user.py:724
msgid "WeCom"
msgstr "企业微信"
#: audits/signal_handlers.py:54 authentication/views/feishu.py:144
-#: authentication/views/login.py:79 notifications/backends/__init__.py:14
-#: users/models/user.py:722
+#: authentication/views/login.py:85 notifications/backends/__init__.py:14
+#: users/models/user.py:726
msgid "FeiShu"
msgstr "飞书"
#: audits/signal_handlers.py:55 authentication/views/dingtalk.py:179
-#: authentication/views/login.py:73 notifications/backends/__init__.py:12
-#: users/models/user.py:721
+#: authentication/views/login.py:79 notifications/backends/__init__.py:12
+#: users/models/user.py:725
msgid "DingTalk"
msgstr "钉钉"
-#: audits/signal_handlers.py:56 authentication/models.py:252
+#: audits/signal_handlers.py:56 authentication/models.py:267
msgid "Temporary token"
msgstr "临时密码"
@@ -1839,6 +1855,10 @@ msgstr "无效的令牌头。符号字符串不应包含无效字符。"
msgid "Invalid token or cache refreshed."
msgstr "刷新的令牌或缓存无效。"
+#: authentication/backends/oauth2/backends.py:155 authentication/models.py:158
+msgid "User invalid, disabled or expired"
+msgstr "用户无效,已禁用或已过期"
+
#: authentication/confirm/password.py:16
msgid "Authentication failed password incorrect"
msgstr "认证失败 (用户名或密码不正确)"
@@ -1889,7 +1909,7 @@ msgstr "没有匹配到认证后端"
#: authentication/errors/const.py:28
msgid "ACL is not allowed"
-msgstr "ACL 不被允许"
+msgstr "登录访问控制不被允许"
#: authentication/errors/const.py:29
msgid "Only local users are allowed"
@@ -1949,23 +1969,19 @@ msgstr "等待登录复核处理"
msgid "Login confirm ticket was {}"
msgstr "登录复核: {}"
-#: authentication/errors/failed.py:145
-msgid "IP is not allowed"
-msgstr "来源 IP 不被允许登录"
+#: authentication/errors/failed.py:146
+msgid "Current IP and Time period is not allowed"
+msgstr "当前 IP 和时间段不被允许登录"
-#: authentication/errors/failed.py:152
-msgid "Time Period is not allowed"
-msgstr "该 时间段 不被允许登录"
-
-#: authentication/errors/failed.py:157
+#: authentication/errors/failed.py:151
msgid "Please enter MFA code"
msgstr "请输入 MFA 验证码"
-#: authentication/errors/failed.py:162
+#: authentication/errors/failed.py:156
msgid "Please enter SMS code"
msgstr "请输入短信验证码"
-#: authentication/errors/failed.py:167 users/exceptions.py:15
+#: authentication/errors/failed.py:161 users/exceptions.py:15
msgid "Phone not set"
msgstr "手机号没有设置"
@@ -1973,19 +1989,37 @@ msgstr "手机号没有设置"
msgid "SSO auth closed"
msgstr "SSO 认证关闭了"
+#: authentication/errors/mfa.py:18 authentication/views/wecom.py:80
+msgid "WeCom is already bound"
+msgstr "企业微信已经绑定"
+
+#: authentication/errors/mfa.py:23 authentication/views/wecom.py:237
+#: authentication/views/wecom.py:291
+msgid "WeCom is not bound"
+msgstr "没有绑定企业微信"
+
+#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:242
+#: authentication/views/dingtalk.py:296
+msgid "DingTalk is not bound"
+msgstr "钉钉没有绑定"
+
+#: authentication/errors/mfa.py:33 authentication/views/feishu.py:203
+msgid "FeiShu is not bound"
+msgstr "没有绑定飞书"
+
#: authentication/errors/mfa.py:38
msgid "Your password is invalid"
msgstr "您的密码无效"
-#: authentication/errors/redirect.py:79 authentication/mixins.py:306
+#: authentication/errors/redirect.py:85 authentication/mixins.py:306
msgid "Your password is too simple, please change it for security"
msgstr "你的密码过于简单,为了安全,请修改"
-#: authentication/errors/redirect.py:87 authentication/mixins.py:313
+#: authentication/errors/redirect.py:93 authentication/mixins.py:313
msgid "You should to change your password before login"
msgstr "登录完成前,请先修改密码"
-#: authentication/errors/redirect.py:95 authentication/mixins.py:320
+#: authentication/errors/redirect.py:101 authentication/mixins.py:320
msgid "Your password has expired, please reset before logging in"
msgstr "您的密码已过期,先修改再登录"
@@ -2061,6 +2095,10 @@ msgstr "设置手机号码启用"
msgid "Clear phone number to disable"
msgstr "清空手机号码禁用"
+#: authentication/middleware.py:76 settings/utils/ldap.py:652
+msgid "Authentication failed (before login check failed): {}"
+msgstr "认证失败(登录前检查失败): {}"
+
#: authentication/mixins.py:256
msgid "The MFA type ({}) is not enabled"
msgstr "该 MFA ({}) 方式没有启用"
@@ -2085,15 +2123,15 @@ msgstr "过期时间"
msgid "SSO token"
msgstr "SSO token"
-#: authentication/models.py:72 authentication/models.py:246
+#: authentication/models.py:72 authentication/models.py:261
#: authentication/templates/authentication/_access_key_modal.html:31
#: settings/serializers/auth/radius.py:17
msgid "Secret"
msgstr "密钥"
-#: authentication/models.py:74 authentication/models.py:249
-#: perms/models/base.py:90 tickets/models/ticket/apply_application.py:26
-#: tickets/models/ticket/apply_asset.py:24 users/models/user.py:703
+#: authentication/models.py:74 authentication/models.py:264
+#: perms/models/base.py:90 tickets/models/ticket/apply_application.py:30
+#: tickets/models/ticket/apply_asset.py:24 users/models/user.py:707
msgid "Date expired"
msgstr "失效日期"
@@ -2109,47 +2147,43 @@ msgstr "连接令牌"
msgid "Can view connection token secret"
msgstr "可以查看连接令牌密文"
-#: authentication/models.py:141
+#: authentication/models.py:149
msgid "Connection token expired at: {}"
msgstr "连接令牌过期: {}"
-#: authentication/models.py:146
+#: authentication/models.py:154
msgid "User not exists"
msgstr "用户不存在"
-#: authentication/models.py:150
-msgid "User invalid, disabled or expired"
-msgstr "用户无效,已禁用或已过期"
-
-#: authentication/models.py:155
+#: authentication/models.py:163
msgid "System user not exists"
msgstr "系统用户不存在"
-#: authentication/models.py:161
+#: authentication/models.py:169
msgid "Asset not exists"
msgstr "资产不存在"
-#: authentication/models.py:165
+#: authentication/models.py:173
msgid "Asset inactive"
msgstr "资产未激活"
-#: authentication/models.py:172
+#: authentication/models.py:180
msgid "User has no permission to access asset or permission expired"
msgstr "用户没有权限访问资产或权限已过期"
-#: authentication/models.py:180
+#: authentication/models.py:188
msgid "Application not exists"
msgstr "应用不存在"
-#: authentication/models.py:187
+#: authentication/models.py:195
msgid "User has no permission to access application or permission expired"
msgstr "用户没有权限访问应用或权限已过期"
-#: authentication/models.py:247
+#: authentication/models.py:262
msgid "Verified"
msgstr "已校验"
-#: authentication/models.py:268
+#: authentication/models.py:283
msgid "Super connection token"
msgstr "超级连接令牌"
@@ -2166,6 +2200,10 @@ msgstr "绑定提醒"
msgid "Validity"
msgstr "有效"
+#: authentication/serializers/connection_token.py:24
+msgid "Expired time"
+msgstr "过期时间"
+
#: authentication/serializers/connection_token.py:73
msgid "Asset or application required"
msgstr "资产或应用必填"
@@ -2246,6 +2284,7 @@ msgid "Need MFA for view auth"
msgstr "需要 MFA 认证来查看账号信息"
#: authentication/templates/authentication/_mfa_confirm_modal.html:20
+#: authentication/templates/authentication/auth_fail_flash_message_standalone.html:37
#: templates/_modal.html:23 templates/flash_message_standalone.html:37
#: users/templates/users/user_password_verify.html:20
msgid "Confirm"
@@ -2260,7 +2299,7 @@ msgstr "代码错误"
#: authentication/templates/authentication/_msg_reset_password.html:3
#: authentication/templates/authentication/_msg_rest_password_success.html:2
#: authentication/templates/authentication/_msg_rest_public_key_success.html:2
-#: jumpserver/conf.py:307 ops/tasks.py:145 ops/tasks.py:148
+#: jumpserver/conf.py:390 ops/tasks.py:145 ops/tasks.py:148
#: perms/templates/perms/_msg_item_permissions_expire.html:3
#: perms/templates/perms/_msg_permed_items_expire.html:3
#: tickets/templates/tickets/approve_check_password.html:33
@@ -2343,13 +2382,18 @@ msgid ""
"security issues"
msgstr "如果这次公钥更新不是由你发起的,那么你的账号可能存在安全问题"
+#: authentication/templates/authentication/auth_fail_flash_message_standalone.html:28
+#: templates/flash_message_standalone.html:28 tickets/const.py:20
+msgid "Cancel"
+msgstr "取消"
+
#: authentication/templates/authentication/login.html:221
msgid "Welcome back, please enter username and password to login"
msgstr "欢迎回来,请输入用户名和密码登录"
#: authentication/templates/authentication/login.html:256
-#: users/templates/users/forgot_password.html:15
#: users/templates/users/forgot_password.html:16
+#: users/templates/users/forgot_password.html:17
msgid "Forgot password"
msgstr "忘记密码"
@@ -2390,10 +2434,15 @@ msgstr "复制链接"
msgid "Return"
msgstr "返回"
-#: authentication/templates/authentication/login_wait_confirm.html:113
+#: authentication/templates/authentication/login_wait_confirm.html:116
msgid "Copy success"
msgstr "复制成功"
+#: authentication/utils.py:28 common/utils/ip/geoip/utils.py:24
+#: xpack/plugins/cloud/const.py:24
+msgid "LAN"
+msgstr "局域网"
+
#: authentication/views/dingtalk.py:41
msgid "DingTalk Error, Please contact your system administrator"
msgstr "钉钉错误,请联系系统管理员"
@@ -2432,10 +2481,6 @@ msgstr "绑定 钉钉 成功"
msgid "Failed to get user from DingTalk"
msgstr "从钉钉获取用户失败"
-#: authentication/views/dingtalk.py:242 authentication/views/dingtalk.py:296
-msgid "DingTalk is not bound"
-msgstr "钉钉没有绑定"
-
#: authentication/views/dingtalk.py:243 authentication/views/dingtalk.py:297
msgid "Please login with a password and then bind the DingTalk"
msgstr "请使用密码登录,然后绑定钉钉"
@@ -2464,27 +2509,23 @@ msgstr "绑定 飞书 成功"
msgid "Failed to get user from FeiShu"
msgstr "从飞书获取用户失败"
-#: authentication/views/feishu.py:203
-msgid "FeiShu is not bound"
-msgstr "没有绑定飞书"
-
#: authentication/views/feishu.py:204
msgid "Please login with a password and then bind the FeiShu"
msgstr "请使用密码登录,然后绑定飞书"
-#: authentication/views/login.py:175
+#: authentication/views/login.py:181
msgid "Redirecting"
msgstr "跳转中"
-#: authentication/views/login.py:176
+#: authentication/views/login.py:182
msgid "Redirecting to {} authentication"
msgstr "正在跳转到 {} 认证"
-#: authentication/views/login.py:199
+#: authentication/views/login.py:205
msgid "Please enable cookies and try again."
msgstr "设置你的浏览器支持cookie"
-#: authentication/views/login.py:301
+#: authentication/views/login.py:307
msgid ""
"Wait for {} confirm, You also can copy link to her/him
\n"
" Don't close this page"
@@ -2492,15 +2533,15 @@ msgstr ""
"等待 {} 确认, 你也可以复制链接发给他/她
\n"
" 不要关闭本页面"
-#: authentication/views/login.py:306
+#: authentication/views/login.py:312
msgid "No ticket found"
msgstr "没有发现工单"
-#: authentication/views/login.py:340
+#: authentication/views/login.py:346
msgid "Logout success"
msgstr "退出登录成功"
-#: authentication/views/login.py:341
+#: authentication/views/login.py:347
msgid "Logout success, return login page"
msgstr "退出登录成功,返回到登录页面"
@@ -2512,10 +2553,6 @@ msgstr "企业微信错误,请联系系统管理员"
msgid "WeCom Error"
msgstr "企业微信错误"
-#: authentication/views/wecom.py:80
-msgid "WeCom is already bound"
-msgstr "企业微信已经绑定"
-
#: authentication/views/wecom.py:163
msgid "WeCom query user failed"
msgstr "企业微信查询用户失败"
@@ -2532,10 +2569,6 @@ msgstr "绑定 企业微信 成功"
msgid "Failed to get user from WeCom"
msgstr "从企业微信获取用户失败"
-#: authentication/views/wecom.py:237 authentication/views/wecom.py:291
-msgid "WeCom is not bound"
-msgstr "没有绑定企业微信"
-
#: authentication/views/wecom.py:238 authentication/views/wecom.py:292
msgid "Please login with a password and then bind the WeCom"
msgstr "请使用密码登录,然后绑定企业微信"
@@ -2664,6 +2697,14 @@ msgstr "企业微信错误,请联系系统管理员"
msgid "Signature does not match"
msgstr "签名不匹配"
+#: common/sdk/sms/cmpp2.py:46
+msgid "sp_id is 6 bits"
+msgstr "SP_id 为6位"
+
+#: common/sdk/sms/cmpp2.py:216
+msgid "Failed to connect to the CMPP gateway server, err: {}"
+msgstr "连接网关服务器错误,错误:{}"
+
#: common/sdk/sms/endpoint.py:16
msgid "Alibaba cloud"
msgstr "阿里云"
@@ -2672,11 +2713,15 @@ msgstr "阿里云"
msgid "Tencent cloud"
msgstr "腾讯云"
-#: common/sdk/sms/endpoint.py:28
+#: common/sdk/sms/endpoint.py:18
+msgid "CMPP v2.0"
+msgstr "CMPP v2.0"
+
+#: common/sdk/sms/endpoint.py:29
msgid "SMS provider not support: {}"
msgstr "短信服务商不支持:{}"
-#: common/sdk/sms/endpoint.py:49
+#: common/sdk/sms/endpoint.py:50
msgid "SMS verification code signature or template invalid"
msgstr "短信验证码签名或模版无效"
@@ -2692,10 +2737,6 @@ msgstr "验证码错误"
msgid "Please wait {} seconds before sending"
msgstr "请在 {} 秒后发送"
-#: common/utils/ip/geoip/utils.py:24
-msgid "LAN"
-msgstr "LAN"
-
#: common/utils/ip/geoip/utils.py:26 common/utils/ip/utils.py:78
msgid "Invalid ip"
msgstr "无效IP"
@@ -2712,11 +2753,11 @@ msgstr "不能包含特殊字符"
msgid "The mobile phone number format is incorrect"
msgstr "手机号格式不正确"
-#: jumpserver/conf.py:306
+#: jumpserver/conf.py:389
msgid "Create account successfully"
msgstr "创建账号成功"
-#: jumpserver/conf.py:308
+#: jumpserver/conf.py:391
msgid "Your account has been created successfully"
msgstr "你的账号已创建成功"
@@ -2756,7 +2797,7 @@ msgid "Notifications"
msgstr "通知"
#: notifications/backends/__init__.py:10 users/forms/profile.py:102
-#: users/models/user.py:663
+#: users/models/user.py:667
msgid "Email"
msgstr "邮件"
@@ -2969,23 +3010,27 @@ msgstr "组织存在资源 ({}) 不能被删除"
msgid "App organizations"
msgstr "组织管理"
-#: orgs/mixins/models.py:46 orgs/mixins/serializers.py:25 orgs/models.py:80
-#: orgs/models.py:211 rbac/const.py:7 rbac/models/rolebinding.py:48
+#: orgs/mixins/models.py:56 orgs/mixins/serializers.py:25 orgs/models.py:85
+#: orgs/models.py:217 rbac/const.py:7 rbac/models/rolebinding.py:48
#: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:62
-#: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:64
+#: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:71
msgid "Organization"
msgstr "组织"
-#: orgs/models.py:74
+#: orgs/models.py:79
msgid "GLOBAL"
msgstr "全局组织"
-#: orgs/models.py:82
+#: orgs/models.py:87
msgid "Can view root org"
msgstr "可以查看全局组织"
-#: orgs/models.py:216 rbac/models/role.py:46 rbac/models/rolebinding.py:44
-#: users/models/user.py:671
+#: orgs/models.py:88
+msgid "Can view all joined org"
+msgstr "可以查看所有加入的组织"
+
+#: orgs/models.py:222 rbac/models/role.py:46 rbac/models/rolebinding.py:44
+#: users/models/user.py:675
msgid "Role"
msgstr "角色"
@@ -3073,27 +3118,32 @@ msgstr "剪贴板复制粘贴"
msgid "From ticket"
msgstr "来自工单"
-#: perms/notifications.py:18
+#: perms/notifications.py:12 perms/notifications.py:44
+#: perms/notifications.py:88 perms/notifications.py:119
+msgid "today"
+msgstr "今"
+
+#: perms/notifications.py:15
msgid "You permed assets is about to expire"
msgstr "你授权的资产即将到期"
-#: perms/notifications.py:23
+#: perms/notifications.py:20
msgid "permed assets"
msgstr "授权的资产"
-#: perms/notifications.py:62
+#: perms/notifications.py:59
msgid "Asset permissions is about to expire"
msgstr "资产授权规则将要过期"
-#: perms/notifications.py:67
+#: perms/notifications.py:64
msgid "asset permissions of organization {}"
msgstr "组织 ({}) 的资产授权"
-#: perms/notifications.py:94
+#: perms/notifications.py:91
msgid "Your permed applications is about to expire"
msgstr "你授权的应用即将过期"
-#: perms/notifications.py:98
+#: perms/notifications.py:95
msgid "permed applications"
msgstr "授权的应用"
@@ -3203,27 +3253,27 @@ msgstr "{} 至少有一个系统角色"
msgid "RBAC"
msgstr "RBAC"
-#: rbac/builtin.py:109
+#: rbac/builtin.py:111
msgid "SystemAdmin"
msgstr "系统管理员"
-#: rbac/builtin.py:112
+#: rbac/builtin.py:114
msgid "SystemAuditor"
msgstr "系统审计员"
-#: rbac/builtin.py:115
+#: rbac/builtin.py:117
msgid "SystemComponent"
msgstr "系统组件"
-#: rbac/builtin.py:121
+#: rbac/builtin.py:123
msgid "OrgAdmin"
msgstr "组织管理员"
-#: rbac/builtin.py:124
+#: rbac/builtin.py:126
msgid "OrgAuditor"
msgstr "组织审计员"
-#: rbac/builtin.py:127
+#: rbac/builtin.py:129
msgid "OrgUser"
msgstr "组织用户"
@@ -3256,6 +3306,7 @@ msgid "Permission"
msgstr "权限"
#: rbac/models/role.py:31 rbac/models/rolebinding.py:38
+#: settings/serializers/auth/oauth2.py:35
msgid "Scope"
msgstr "范围"
@@ -3279,17 +3330,21 @@ msgstr "组织角色"
msgid "Role binding"
msgstr "角色绑定"
-#: rbac/models/rolebinding.py:159
+#: rbac/models/rolebinding.py:137
+msgid "All organizations"
+msgstr "所有组织"
+
+#: rbac/models/rolebinding.py:166
msgid ""
"User last role in org, can not be delete, you can remove user from org "
"instead"
msgstr "用户最后一个角色,不能删除,你可以将用户从组织移除"
-#: rbac/models/rolebinding.py:166
+#: rbac/models/rolebinding.py:173
msgid "Organization role binding"
msgstr "组织角色绑定"
-#: rbac/models/rolebinding.py:181
+#: rbac/models/rolebinding.py:188
msgid "System role binding"
msgstr "系统角色绑定"
@@ -3329,14 +3384,10 @@ msgstr "工作台"
msgid "Audit view"
msgstr "审计台"
-#: rbac/tree.py:28 settings/models.py:140
+#: rbac/tree.py:28 settings/models.py:156
msgid "System setting"
msgstr "系统设置"
-#: rbac/tree.py:29
-msgid "Other"
-msgstr "其它"
-
#: rbac/tree.py:37
msgid "Accounts"
msgstr "账号管理"
@@ -3393,13 +3444,8 @@ msgstr "查看授权树"
msgid "Execute batch command"
msgstr "执行批量命令"
-#: settings/api/alibaba_sms.py:31 settings/api/tencent_sms.py:35
-msgid "test_phone is required"
-msgstr "测试手机号 该字段是必填项。"
-
-#: settings/api/alibaba_sms.py:52 settings/api/dingtalk.py:31
-#: settings/api/feishu.py:36 settings/api/tencent_sms.py:57
-#: settings/api/wecom.py:37
+#: settings/api/dingtalk.py:31 settings/api/feishu.py:36
+#: settings/api/sms.py:131 settings/api/wecom.py:37
msgid "Test success"
msgstr "测试成功"
@@ -3427,47 +3473,55 @@ msgstr "获取 LDAP 用户为 None"
msgid "Imported {} users successfully (Organization: {})"
msgstr "成功导入 {} 个用户 ( 组织: {} )"
+#: settings/api/sms.py:113
+msgid "Invalid SMS platform"
+msgstr "无效的短信平台"
+
+#: settings/api/sms.py:119
+msgid "test_phone is required"
+msgstr "测试手机号 该字段是必填项。"
+
#: settings/apps.py:7
msgid "Settings"
msgstr "系统设置"
-#: settings/models.py:142
+#: settings/models.py:158
msgid "Can change email setting"
msgstr "邮件设置"
-#: settings/models.py:143
+#: settings/models.py:159
msgid "Can change auth setting"
msgstr "认证设置"
-#: settings/models.py:144
+#: settings/models.py:160
msgid "Can change system msg sub setting"
msgstr "消息订阅设置"
-#: settings/models.py:145
+#: settings/models.py:161
msgid "Can change sms setting"
msgstr "短信设置"
-#: settings/models.py:146
+#: settings/models.py:162
msgid "Can change security setting"
msgstr "安全设置"
-#: settings/models.py:147
+#: settings/models.py:163
msgid "Can change clean setting"
msgstr "定期清理"
-#: settings/models.py:148
+#: settings/models.py:164
msgid "Can change interface setting"
msgstr "界面设置"
-#: settings/models.py:149
+#: settings/models.py:165
msgid "Can change license setting"
msgstr "许可证设置"
-#: settings/models.py:150
+#: settings/models.py:166
msgid "Can change terminal setting"
msgstr "终端设置"
-#: settings/models.py:151
+#: settings/models.py:167
msgid "Can change other setting"
msgstr "其它设置"
@@ -3580,7 +3634,8 @@ msgstr "用户过滤器"
msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)"
msgstr "可能的选项是(cn或uid或sAMAccountName=%(user)s)"
-#: settings/serializers/auth/ldap.py:57 settings/serializers/auth/oidc.py:36
+#: settings/serializers/auth/ldap.py:57 settings/serializers/auth/oauth2.py:51
+#: settings/serializers/auth/oidc.py:36
msgid "User attr map"
msgstr "用户属性映射"
@@ -3604,23 +3659,52 @@ msgstr "搜索分页数量"
msgid "Enable LDAP auth"
msgstr "启用 LDAP 认证"
-#: settings/serializers/auth/oidc.py:15
-msgid "Base site url"
-msgstr "JumpServer 地址"
+#: settings/serializers/auth/oauth2.py:20
+msgid "Enable OAuth2 Auth"
+msgstr "启用 OAuth2 认证"
-#: settings/serializers/auth/oidc.py:18
+#: settings/serializers/auth/oauth2.py:23
+msgid "Logo"
+msgstr "图标"
+
+#: settings/serializers/auth/oauth2.py:26
+msgid "Service provider"
+msgstr "服务提供商"
+
+#: settings/serializers/auth/oauth2.py:29 settings/serializers/auth/oidc.py:18
msgid "Client Id"
msgstr "客户端 ID"
-#: settings/serializers/auth/oidc.py:21
-#: xpack/plugins/cloud/serializers/account_attrs.py:36
+#: settings/serializers/auth/oauth2.py:32 settings/serializers/auth/oidc.py:21
+#: xpack/plugins/cloud/serializers/account_attrs.py:38
msgid "Client Secret"
msgstr "客户端密钥"
-#: settings/serializers/auth/oidc.py:29
+#: settings/serializers/auth/oauth2.py:38 settings/serializers/auth/oidc.py:62
+msgid "Provider auth endpoint"
+msgstr "授权端点地址"
+
+#: settings/serializers/auth/oauth2.py:41 settings/serializers/auth/oidc.py:65
+msgid "Provider token endpoint"
+msgstr "token 端点地址"
+
+#: settings/serializers/auth/oauth2.py:44 settings/serializers/auth/oidc.py:29
msgid "Client authentication method"
msgstr "客户端认证方式"
+#: settings/serializers/auth/oauth2.py:48 settings/serializers/auth/oidc.py:71
+msgid "Provider userinfo endpoint"
+msgstr "用户信息端点地址"
+
+#: settings/serializers/auth/oauth2.py:54 settings/serializers/auth/oidc.py:92
+#: settings/serializers/auth/saml2.py:33
+msgid "Always update user"
+msgstr "总是更新用户信息"
+
+#: settings/serializers/auth/oidc.py:15
+msgid "Base site url"
+msgstr "JumpServer 地址"
+
#: settings/serializers/auth/oidc.py:31
msgid "Share session"
msgstr "共享会话"
@@ -3653,22 +3737,10 @@ msgstr "启用 OIDC 认证"
msgid "Provider endpoint"
msgstr "端点地址"
-#: settings/serializers/auth/oidc.py:62
-msgid "Provider auth endpoint"
-msgstr "授权端点地址"
-
-#: settings/serializers/auth/oidc.py:65
-msgid "Provider token endpoint"
-msgstr "token 端点地址"
-
#: settings/serializers/auth/oidc.py:68
msgid "Provider jwks endpoint"
msgstr "jwks 端点地址"
-#: settings/serializers/auth/oidc.py:71
-msgid "Provider userinfo endpoint"
-msgstr "用户信息端点地址"
-
#: settings/serializers/auth/oidc.py:74
msgid "Provider end session endpoint"
msgstr "注销会话端点地址"
@@ -3701,10 +3773,6 @@ msgstr "使用状态"
msgid "Use nonce"
msgstr "临时使用"
-#: settings/serializers/auth/oidc.py:92 settings/serializers/auth/saml2.py:33
-msgid "Always update user"
-msgstr "总是更新用户信息"
-
#: settings/serializers/auth/radius.py:13
msgid "Enable Radius Auth"
msgstr "启用 Radius 认证"
@@ -3737,41 +3805,82 @@ msgstr "SP 密钥"
msgid "SP cert"
msgstr "SP 证书"
-#: settings/serializers/auth/sms.py:11
+#: settings/serializers/auth/sms.py:15
msgid "Enable SMS"
msgstr "启用 SMS"
-#: settings/serializers/auth/sms.py:13
-msgid "SMS provider"
-msgstr "短信服务商"
+#: settings/serializers/auth/sms.py:17
+msgid "SMS provider / Protocol"
+msgstr "短信服务商 / 协议"
-#: settings/serializers/auth/sms.py:18 settings/serializers/auth/sms.py:36
-#: settings/serializers/auth/sms.py:44 settings/serializers/email.py:65
+#: settings/serializers/auth/sms.py:22 settings/serializers/auth/sms.py:43
+#: settings/serializers/auth/sms.py:51 settings/serializers/auth/sms.py:62
+#: settings/serializers/email.py:65
msgid "Signature"
msgstr "签名"
-#: settings/serializers/auth/sms.py:19 settings/serializers/auth/sms.py:37
-#: settings/serializers/auth/sms.py:45
+#: settings/serializers/auth/sms.py:23 settings/serializers/auth/sms.py:44
+#: settings/serializers/auth/sms.py:52
msgid "Template code"
msgstr "模板"
-#: settings/serializers/auth/sms.py:23
+#: settings/serializers/auth/sms.py:29
msgid "Test phone"
msgstr "测试手机号"
-#: settings/serializers/auth/sso.py:12
+#: settings/serializers/auth/sms.py:58
+msgid "Enterprise code(SP id)"
+msgstr "企业代码(SP id)"
+
+#: settings/serializers/auth/sms.py:59
+msgid "Shared secret(Shared secret)"
+msgstr "共享密码(Shared secret)"
+
+#: settings/serializers/auth/sms.py:60
+msgid "Original number(Src id)"
+msgstr "原始号码(Src id)"
+
+#: settings/serializers/auth/sms.py:61
+msgid "Business type(Service id)"
+msgstr "业务类型(Service id)"
+
+#: settings/serializers/auth/sms.py:64
+msgid "Template"
+msgstr "模板"
+
+#: settings/serializers/auth/sms.py:65
+#, python-brace-format
+msgid ""
+"Template need contain {code} and Signature + template length does not exceed "
+"67 words. For example, your verification code is {code}, which is valid for "
+"5 minutes. Please do not disclose it to others."
+msgstr ""
+"模板需要包含 {code},并且模板+签名长度不能超过67个字。例如, 您的验证码是 "
+"{code}, 有效期为5分钟。请不要泄露给其他人。"
+
+#: settings/serializers/auth/sms.py:74
+#, python-brace-format
+msgid "The template needs to contain {code}"
+msgstr "模板需要包含 {code}"
+
+#: settings/serializers/auth/sms.py:77
+msgid "Signature + Template must not exceed 65 words"
+msgstr "模板+签名不能超过65个字"
+
+#: settings/serializers/auth/sso.py:11
msgid "Enable SSO auth"
msgstr "启用 SSO Token 认证"
-#: settings/serializers/auth/sso.py:13
+#: settings/serializers/auth/sso.py:12
msgid "Other service can using SSO token login to JumpServer without password"
msgstr "其它系统可以使用 SSO Token 对接 JumpServer, 免去登录的过程"
-#: settings/serializers/auth/sso.py:16
+#: settings/serializers/auth/sso.py:15
msgid "SSO auth key TTL"
msgstr "Token 有效期"
-#: settings/serializers/auth/sso.py:16
+#: settings/serializers/auth/sso.py:15
+#: xpack/plugins/cloud/serializers/account_attrs.py:159
msgid "Unit: second"
msgstr "单位: 秒"
@@ -3837,7 +3946,7 @@ msgstr "登录日志"
#: settings/serializers/cleaning.py:10 settings/serializers/cleaning.py:14
#: settings/serializers/cleaning.py:18 settings/serializers/cleaning.py:22
-#: settings/serializers/cleaning.py:26
+#: settings/serializers/cleaning.py:26 settings/serializers/other.py:35
msgid "Unit: day"
msgstr "单位: 天"
@@ -4002,19 +4111,23 @@ msgstr ""
"放置单独授权的资产到未分组节点, 避免能看到资产所在节点,但该节点未被授权的问"
"题"
-#: settings/serializers/other.py:34
+#: settings/serializers/other.py:35
+msgid "Ticket authorize default time"
+msgstr "默认工单授权时间"
+
+#: settings/serializers/other.py:39
msgid "Help Docs URL"
msgstr "文档链接"
-#: settings/serializers/other.py:35
+#: settings/serializers/other.py:40
msgid "default: http://docs.jumpserver.org"
msgstr "默认: http://dev.jumpserver.org:8080"
-#: settings/serializers/other.py:39
+#: settings/serializers/other.py:44
msgid "Help Support URL"
msgstr "支持链接"
-#: settings/serializers/other.py:40
+#: settings/serializers/other.py:45
msgid "default: http://www.jumpserver.org/support/"
msgstr "默认: http://www.jumpserver.org/support/"
@@ -4238,7 +4351,7 @@ msgstr "会话分享"
#: settings/serializers/security.py:180
msgid "Enabled, Allows user active session to be shared with other users"
-msgstr "开启后允许用户分享已连接的资产会话给它人,协同工作"
+msgstr "开启后允许用户分享已连接的资产会话给他人,协同工作"
#: settings/serializers/security.py:183
msgid "Remote Login Protection"
@@ -4389,10 +4502,6 @@ msgstr "成功匹配 {} 个用户"
msgid "Authentication failed (configuration incorrect): {}"
msgstr "认证失败(配置错误): {}"
-#: settings/utils/ldap.py:652
-msgid "Authentication failed (before login check failed): {}"
-msgstr "认证失败(登录前检查失败): {}"
-
#: settings/utils/ldap.py:654
msgid "Authentication failed (username or password incorrect): {}"
msgstr "认证失败 (用户名或密码不正确): {}"
@@ -4558,10 +4667,6 @@ msgstr "验证码已发送"
msgid "Home page"
msgstr "首页"
-#: templates/flash_message_standalone.html:28 tickets/const.py:20
-msgid "Cancel"
-msgstr "取消"
-
#: templates/resource_download.html:18 templates/resource_download.html:31
msgid "Client"
msgstr "客户端"
@@ -4602,7 +4707,7 @@ msgstr "Jmservisor 是在 windows 远程应用发布服务器中用来拉起远
msgid "Offline video player"
msgstr "离线录像播放器"
-#: terminal/api/endpoint.py:26
+#: terminal/api/endpoint.py:34
msgid "Not found protocol query params"
msgstr ""
@@ -4688,7 +4793,7 @@ msgid "Output"
msgstr "输出"
#: terminal/backends/command/models.py:25 terminal/models/replay.py:9
-#: terminal/models/sharing.py:19 terminal/models/sharing.py:76
+#: terminal/models/sharing.py:19 terminal/models/sharing.py:78
#: terminal/templates/terminal/_msg_command_alert.html:10
#: tickets/models/ticket/command_confirm.py:20
msgid "Session"
@@ -4739,7 +4844,7 @@ msgstr "不支持批量创建"
msgid "Storage is invalid"
msgstr "存储无效"
-#: terminal/models/command.py:53
+#: terminal/models/command.py:66
msgid "Command record"
msgstr "命令记录"
@@ -4775,18 +4880,26 @@ msgstr "PostgreSQL 端口"
msgid "Redis Port"
msgstr "Redis 端口"
-#: terminal/models/endpoint.py:26 terminal/models/endpoint.py:90
-#: terminal/serializers/endpoint.py:45 terminal/serializers/storage.py:38
+#: terminal/models/endpoint.py:21
+msgid "Oracle 11g Port"
+msgstr "Oracle 11g 端口"
+
+#: terminal/models/endpoint.py:22
+msgid "Oracle 12c Port"
+msgstr "Oracle 12c 端口"
+
+#: terminal/models/endpoint.py:28 terminal/models/endpoint.py:94
+#: terminal/serializers/endpoint.py:57 terminal/serializers/storage.py:38
#: terminal/serializers/storage.py:50 terminal/serializers/storage.py:80
#: terminal/serializers/storage.py:90 terminal/serializers/storage.py:98
msgid "Endpoint"
msgstr "端点"
-#: terminal/models/endpoint.py:83
+#: terminal/models/endpoint.py:87
msgid "IP group"
msgstr "IP 组"
-#: terminal/models/endpoint.py:95
+#: terminal/models/endpoint.py:99
msgid "Endpoint rule"
msgstr "端点规则"
@@ -4802,7 +4915,7 @@ msgstr "可以上传会话录像"
msgid "Can download session replay"
msgstr "可以下载会话录像"
-#: terminal/models/session.py:50 terminal/models/sharing.py:99
+#: terminal/models/session.py:50 terminal/models/sharing.py:101
msgid "Login from"
msgstr "登录来源"
@@ -4838,7 +4951,7 @@ msgstr "可以验证会话动作权限"
msgid "Creator"
msgstr "创建者"
-#: terminal/models/sharing.py:26 terminal/models/sharing.py:78
+#: terminal/models/sharing.py:26 terminal/models/sharing.py:80
msgid "Verify code"
msgstr "验证码"
@@ -4846,7 +4959,7 @@ msgstr "验证码"
msgid "Expired time (min)"
msgstr "过期时间 (分)"
-#: terminal/models/sharing.py:37 terminal/models/sharing.py:81
+#: terminal/models/sharing.py:37 terminal/models/sharing.py:83
msgid "Session sharing"
msgstr "会话分享"
@@ -4854,40 +4967,40 @@ msgstr "会话分享"
msgid "Can add super session sharing"
msgstr "可以创建超级会话分享"
-#: terminal/models/sharing.py:64
+#: terminal/models/sharing.py:66
msgid "Link not active"
msgstr "链接失效"
-#: terminal/models/sharing.py:66
+#: terminal/models/sharing.py:68
msgid "Link expired"
msgstr "链接过期"
-#: terminal/models/sharing.py:68
+#: terminal/models/sharing.py:70
msgid "User not allowed to join"
-msgstr "来源 IP 不被允许登录"
+msgstr "该用户无权加入会话"
-#: terminal/models/sharing.py:85 terminal/serializers/sharing.py:59
+#: terminal/models/sharing.py:87 terminal/serializers/sharing.py:59
msgid "Joiner"
msgstr "加入者"
-#: terminal/models/sharing.py:88
+#: terminal/models/sharing.py:90
msgid "Date joined"
msgstr "加入日期"
-#: terminal/models/sharing.py:91
+#: terminal/models/sharing.py:93
msgid "Date left"
msgstr "结束日期"
-#: terminal/models/sharing.py:109 tickets/const.py:26
+#: terminal/models/sharing.py:111 tickets/const.py:26
#: xpack/plugins/change_auth_plan/models/base.py:192
msgid "Finished"
msgstr "结束"
-#: terminal/models/sharing.py:114
+#: terminal/models/sharing.py:116
msgid "Session join record"
msgstr "会话加入记录"
-#: terminal/models/sharing.py:130
+#: terminal/models/sharing.py:132
msgid "Invalid verification code"
msgstr "验证码不正确"
@@ -4943,7 +5056,7 @@ msgstr "其它参数"
msgid "type"
msgstr "类型"
-#: terminal/models/terminal.py:183 terminal/serializers/session.py:39
+#: terminal/models/terminal.py:183
msgid "Terminal"
msgstr "终端"
@@ -4967,7 +5080,11 @@ msgstr "级别"
msgid "Batch danger command alert"
msgstr "批量危险命令告警"
-#: terminal/serializers/endpoint.py:39
+#: terminal/serializers/endpoint.py:12
+msgid "Oracle port"
+msgstr ""
+
+#: terminal/serializers/endpoint.py:51
msgid ""
"If asset IP addresses under different endpoints conflict, use asset labels"
msgstr "如果不同端点下的资产 IP 有冲突,使用资产标签实现"
@@ -5000,6 +5117,10 @@ msgstr "是否可重放"
msgid "Can join"
msgstr "是否可加入"
+#: terminal/serializers/session.py:39
+msgid "Terminal ID"
+msgstr "终端 ID"
+
#: terminal/serializers/session.py:41
msgid "Can terminate"
msgstr "是否可中断"
@@ -5017,12 +5138,12 @@ msgid "Bucket"
msgstr "桶名称"
#: terminal/serializers/storage.py:30
-#: xpack/plugins/cloud/serializers/account_attrs.py:15
+#: xpack/plugins/cloud/serializers/account_attrs.py:17
msgid "Access key id"
msgstr "访问密钥 ID(AK)"
#: terminal/serializers/storage.py:34
-#: xpack/plugins/cloud/serializers/account_attrs.py:18
+#: xpack/plugins/cloud/serializers/account_attrs.py:20
msgid "Access key secret"
msgstr "访问密钥密文(SK)"
@@ -5162,33 +5283,33 @@ msgstr "自定义用户"
msgid "Ticket already closed"
msgstr "工单已经关闭"
-#: tickets/handlers/apply_application.py:35
+#: tickets/handlers/apply_application.py:38
msgid ""
"Created by the ticket, ticket title: {}, ticket applicant: {}, ticket "
"processor: {}, ticket ID: {}"
msgstr ""
"通过工单创建, 工单标题: {}, 工单申请人: {}, 工单处理人: {}, 工单 ID: {}"
-#: tickets/handlers/apply_asset.py:35
+#: tickets/handlers/apply_asset.py:37
msgid ""
"Created by the ticket ticket title: {} ticket applicant: {} ticket "
"processor: {} ticket ID: {}"
msgstr ""
"通过工单创建, 工单标题: {}, 工单申请人: {}, 工单处理人: {}, 工单 ID: {}"
-#: tickets/handlers/base.py:79
+#: tickets/handlers/base.py:84
msgid "Change field"
msgstr "变更字段"
-#: tickets/handlers/base.py:79
+#: tickets/handlers/base.py:84
msgid "Before change"
msgstr "变更前"
-#: tickets/handlers/base.py:79
+#: tickets/handlers/base.py:84
msgid "After change"
msgstr "变更后"
-#: tickets/handlers/base.py:91
+#: tickets/handlers/base.py:96
msgid "{} {} the ticket"
msgstr "{} {} 工单"
@@ -5226,11 +5347,11 @@ msgstr "内容"
msgid "Approve level"
msgstr "审批级别"
-#: tickets/models/flow.py:25 tickets/serializers/flow.py:14
+#: tickets/models/flow.py:25 tickets/serializers/flow.py:15
msgid "Approve strategy"
msgstr "审批策略"
-#: tickets/models/flow.py:30 tickets/serializers/flow.py:15
+#: tickets/models/flow.py:30 tickets/serializers/flow.py:16
msgid "Assignees"
msgstr "受理人"
@@ -5246,16 +5367,16 @@ msgstr "工单流程"
msgid "Ticket session relation"
msgstr "工单会话"
-#: tickets/models/ticket/apply_application.py:11
+#: tickets/models/ticket/apply_application.py:12
#: tickets/models/ticket/apply_asset.py:13
msgid "Permission name"
msgstr "授权规则名称"
-#: tickets/models/ticket/apply_application.py:20
+#: tickets/models/ticket/apply_application.py:21
msgid "Apply applications"
msgstr "申请应用"
-#: tickets/models/ticket/apply_application.py:23
+#: tickets/models/ticket/apply_application.py:24
#: tickets/models/ticket/apply_asset.py:18
msgid "Apply system users"
msgstr "申请的系统用户"
@@ -5373,15 +5494,15 @@ msgstr "你的工单已被处理, 处理人 - {}"
msgid "Ticket has processed - {} ({})"
msgstr "你的工单已被处理, 处理人 - {} ({})"
-#: tickets/serializers/flow.py:16
+#: tickets/serializers/flow.py:17
msgid "Assignees display"
msgstr "受理人名称"
-#: tickets/serializers/flow.py:42
+#: tickets/serializers/flow.py:43
msgid "Please select the Assignees"
msgstr "请选择受理人"
-#: tickets/serializers/flow.py:68
+#: tickets/serializers/flow.py:69
msgid "The current organization type already exists"
msgstr "当前组织已存在该类型"
@@ -5402,7 +5523,7 @@ msgstr "过期时间要大于开始时间"
msgid "Permission named `{}` already exists"
msgstr "授权名称 `{}` 已存在"
-#: tickets/serializers/ticket/ticket.py:92
+#: tickets/serializers/ticket/ticket.py:99
msgid "The ticket flow `{}` does not exist"
msgstr "工单流程 `{}` 不存在"
@@ -5559,7 +5680,7 @@ msgstr "不能和原来的密钥相同"
msgid "Not a valid ssh public key"
msgstr "SSH 密钥不合法"
-#: users/forms/profile.py:161 users/models/user.py:692
+#: users/forms/profile.py:161 users/models/user.py:696
msgid "Public key"
msgstr "SSH 公钥"
@@ -5571,55 +5692,55 @@ msgstr "强制启用"
msgid "Local"
msgstr "数据库"
-#: users/models/user.py:673 users/serializers/user.py:149
+#: users/models/user.py:677 users/serializers/user.py:149
msgid "Is service account"
msgstr "服务账号"
-#: users/models/user.py:675
+#: users/models/user.py:679
msgid "Avatar"
msgstr "头像"
-#: users/models/user.py:678
+#: users/models/user.py:682
msgid "Wechat"
msgstr "微信"
-#: users/models/user.py:695
+#: users/models/user.py:699
msgid "Secret key"
msgstr "Secret key"
-#: users/models/user.py:711
+#: users/models/user.py:715
msgid "Source"
msgstr "来源"
-#: users/models/user.py:715
+#: users/models/user.py:719
msgid "Date password last updated"
msgstr "最后更新密码日期"
-#: users/models/user.py:718
+#: users/models/user.py:722
msgid "Need update password"
msgstr "需要更新密码"
-#: users/models/user.py:892
+#: users/models/user.py:896
msgid "Can invite user"
msgstr "可以邀请用户"
-#: users/models/user.py:893
+#: users/models/user.py:897
msgid "Can remove user"
msgstr "可以移除用户"
-#: users/models/user.py:894
+#: users/models/user.py:898
msgid "Can match user"
msgstr "可以匹配用户"
-#: users/models/user.py:903
+#: users/models/user.py:907
msgid "Administrator"
msgstr "管理员"
-#: users/models/user.py:906
+#: users/models/user.py:910
msgid "Administrator is the super user of system"
msgstr "Administrator是初始的超级管理员"
-#: users/models/user.py:931
+#: users/models/user.py:935
msgid "User password history"
msgstr "用户密码历史"
@@ -5824,11 +5945,11 @@ msgstr "你的 SSH 密钥已经被管理员重置"
msgid "click here to set your password"
msgstr "点击这里设置密码"
-#: users/templates/users/forgot_password.html:23
+#: users/templates/users/forgot_password.html:24
msgid "Input your email, that will send a mail to your"
msgstr "输入您的邮箱, 将会发一封重置邮件到您的邮箱中"
-#: users/templates/users/forgot_password.html:32
+#: users/templates/users/forgot_password.html:33
msgid "Submit"
msgstr "提交"
@@ -6260,31 +6381,31 @@ msgstr "谷歌云"
msgid "Fusion Compute"
msgstr ""
-#: xpack/plugins/cloud/const.py:27
+#: xpack/plugins/cloud/const.py:28
msgid "Instance name"
msgstr "实例名称"
-#: xpack/plugins/cloud/const.py:28
+#: xpack/plugins/cloud/const.py:29
msgid "Instance name and Partial IP"
msgstr "实例名称和部分IP"
-#: xpack/plugins/cloud/const.py:33
+#: xpack/plugins/cloud/const.py:34
msgid "Succeed"
msgstr "成功"
-#: xpack/plugins/cloud/const.py:37
+#: xpack/plugins/cloud/const.py:38
msgid "Unsync"
msgstr "未同步"
-#: xpack/plugins/cloud/const.py:38
+#: xpack/plugins/cloud/const.py:39
msgid "New Sync"
msgstr "新同步"
-#: xpack/plugins/cloud/const.py:39
+#: xpack/plugins/cloud/const.py:40
msgid "Synced"
msgstr "已同步"
-#: xpack/plugins/cloud/const.py:40
+#: xpack/plugins/cloud/const.py:41
msgid "Released"
msgstr "已释放"
@@ -6554,52 +6675,87 @@ msgstr "华南-广州-友好用户环境"
msgid "CN East-Suqian"
msgstr "华东-宿迁"
-#: xpack/plugins/cloud/serializers/account.py:61
+#: xpack/plugins/cloud/serializers/account.py:62
msgid "Validity display"
msgstr "有效性显示"
-#: xpack/plugins/cloud/serializers/account.py:62
+#: xpack/plugins/cloud/serializers/account.py:63
msgid "Provider display"
msgstr "服务商显示"
-#: xpack/plugins/cloud/serializers/account_attrs.py:33
+#: xpack/plugins/cloud/serializers/account_attrs.py:35
msgid "Client ID"
msgstr "客户端 ID"
-#: xpack/plugins/cloud/serializers/account_attrs.py:39
+#: xpack/plugins/cloud/serializers/account_attrs.py:41
msgid "Tenant ID"
msgstr "租户 ID"
-#: xpack/plugins/cloud/serializers/account_attrs.py:42
+#: xpack/plugins/cloud/serializers/account_attrs.py:44
msgid "Subscription ID"
msgstr "订阅 ID"
-#: xpack/plugins/cloud/serializers/account_attrs.py:93
-#: xpack/plugins/cloud/serializers/account_attrs.py:98
-#: xpack/plugins/cloud/serializers/account_attrs.py:122
+#: xpack/plugins/cloud/serializers/account_attrs.py:95
+#: xpack/plugins/cloud/serializers/account_attrs.py:100
+#: xpack/plugins/cloud/serializers/account_attrs.py:124
msgid "API Endpoint"
msgstr "API 端点"
-#: xpack/plugins/cloud/serializers/account_attrs.py:104
+#: xpack/plugins/cloud/serializers/account_attrs.py:106
msgid "Auth url"
msgstr "认证地址"
-#: xpack/plugins/cloud/serializers/account_attrs.py:105
+#: xpack/plugins/cloud/serializers/account_attrs.py:107
msgid "eg: http://openstack.example.com:5000/v3"
msgstr "如: http://openstack.example.com:5000/v3"
-#: xpack/plugins/cloud/serializers/account_attrs.py:108
+#: xpack/plugins/cloud/serializers/account_attrs.py:110
msgid "User domain"
msgstr "用户域"
-#: xpack/plugins/cloud/serializers/account_attrs.py:115
+#: xpack/plugins/cloud/serializers/account_attrs.py:117
msgid "Service account key"
msgstr "服务账号密钥"
-#: xpack/plugins/cloud/serializers/account_attrs.py:116
+#: xpack/plugins/cloud/serializers/account_attrs.py:118
msgid "The file is in JSON format"
msgstr "JSON 格式的文件"
+#: xpack/plugins/cloud/serializers/account_attrs.py:131
+msgid "IP address invalid `{}`, {}"
+msgstr "IP 地址无效: `{}`, {}"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:137
+msgid ""
+"Format for comma-delimited string,Such as: 192.168.1.0/24, "
+"10.0.0.0-10.0.0.255"
+msgstr "格式为逗号分隔的字符串,如:192.168.1.0/24,10.0.0.0-10.0.0.255"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:141
+msgid ""
+"The port is used to detect the validity of the IP address. When the "
+"synchronization task is executed, only the valid IP address will be "
+"synchronized.
If the port is 0, all IP addresses are valid."
+msgstr ""
+"端口用来检测 IP 地址的有效性,在同步任务执行时,只会同步有效的 IP 地址。
"
+"如果端口为 0,则表示所有 IP 地址均有效。"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:149
+msgid "Hostname prefix"
+msgstr "主机名前缀"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:152
+msgid "IP segment"
+msgstr "IP 网段"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:156
+msgid "Test port"
+msgstr "测试端口"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:159
+msgid "Test timeout"
+msgstr "测试超时时间"
+
#: xpack/plugins/cloud/serializers/task.py:29
msgid ""
"Only instances matching the IP range will be synced.
If the instance "
@@ -6608,10 +6764,9 @@ msgid ""
"all instances and randomly match IP addresses.
Format for comma-"
"delimited string, Such as: 192.168.1.0/24, 10.1.1.1-10.1.1.20"
msgstr ""
-"只有匹配到 IP 段的实例会被同步。
"
-"如果实例包含多个 IP 地址,那么第一个匹配到的 IP 地址将被用作创建的资产的 IP。
"
-"默认值 * 表示同步所有实例和随机匹配 IP 地址。
"
-"格式为以逗号分隔的字符串,例如:192.168.1.0/24,10.1.1.1-10.1.1.20"
+"只有匹配到 IP 段的实例会被同步。
如果实例包含多个 IP 地址,那么第一个匹配"
+"到的 IP 地址将被用作创建的资产的 IP。
默认值 * 表示同步所有实例和随机匹配 "
+"IP 地址。
格式为以逗号分隔的字符串,例如:192.168.1.0/24,10.1.1.1-10.1.1.20"
#: xpack/plugins/cloud/serializers/task.py:36
msgid "History count"
@@ -6717,6 +6872,3 @@ msgstr "旗舰版"
#: xpack/plugins/license/models.py:77
msgid "Community edition"
msgstr "社区版"
-
-#~ msgid "User cannot self-update fields: {}"
-#~ msgstr "用户不能更新自己的字段: {}"
diff --git a/apps/ops/inventory.py b/apps/ops/inventory.py
index c2b886413..45b174228 100644
--- a/apps/ops/inventory.py
+++ b/apps/ops/inventory.py
@@ -15,8 +15,6 @@ logger = get_logger(__file__)
class JMSBaseInventory(BaseInventory):
- windows_ssh_default_shell = settings.WINDOWS_SSH_DEFAULT_SHELL
-
def convert_to_ansible(self, asset, run_as_admin=False):
info = {
'id': asset.id,
@@ -33,7 +31,7 @@ class JMSBaseInventory(BaseInventory):
if asset.is_windows():
info["vars"].update({
"ansible_connection": "ssh",
- "ansible_shell_type": self.windows_ssh_default_shell,
+ "ansible_shell_type": settings.WINDOWS_SSH_DEFAULT_SHELL,
})
for label in asset.labels.all():
info["vars"].update({
diff --git a/apps/orgs/migrations/0013_alter_organization_options.py b/apps/orgs/migrations/0013_alter_organization_options.py
new file mode 100644
index 000000000..e868a87a3
--- /dev/null
+++ b/apps/orgs/migrations/0013_alter_organization_options.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.2.12 on 2022-07-18 05:57
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('orgs', '0012_auto_20220118_1054'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='organization',
+ options={'permissions': (('view_rootorg', 'Can view root org'), ('view_alljoinedorg', 'Can view all joined org')), 'verbose_name': 'Organization'},
+ ),
+ ]
diff --git a/apps/orgs/mixins/models.py b/apps/orgs/mixins/models.py
index 48ed09af7..63b78bd23 100644
--- a/apps/orgs/mixins/models.py
+++ b/apps/orgs/mixins/models.py
@@ -45,7 +45,7 @@ class OrgManager(models.Manager):
org = get_current_org()
for obj in objs:
if org.is_root():
- if not self.org_id:
+ if not obj.org_id:
raise ValidationError('Please save in a organization')
else:
obj.org_id = org.id
diff --git a/apps/orgs/models.py b/apps/orgs/models.py
index 430a75a5b..d5e3ae617 100644
--- a/apps/orgs/models.py
+++ b/apps/orgs/models.py
@@ -16,9 +16,14 @@ class OrgRoleMixin:
def add_member(self, user, role=None):
from rbac.builtin import BuiltinRole
from .utils import tmp_to_org
- role_id = BuiltinRole.org_user.id
+
if role:
role_id = role.id
+ elif user.is_service_account:
+ role_id = BuiltinRole.system_component.id
+ else:
+ role_id = BuiltinRole.org_user.id
+
with tmp_to_org(self):
defaults = {
'user': user, 'role_id': role_id,
@@ -80,6 +85,7 @@ class Organization(OrgRoleMixin, models.Model):
verbose_name = _("Organization")
permissions = (
('view_rootorg', _('Can view root org')),
+ ('view_alljoinedorg', _('Can view all joined org')),
)
def __str__(self):
diff --git a/apps/orgs/signal_handlers/common.py b/apps/orgs/signal_handlers/common.py
index f2396231b..7b825b748 100644
--- a/apps/orgs/signal_handlers/common.py
+++ b/apps/orgs/signal_handlers/common.py
@@ -145,6 +145,9 @@ def _clear_users_from_org(org, users):
@receiver(post_save, sender=User)
@on_transaction_commit
def on_user_created_set_default_org(sender, instance, created, **kwargs):
+ if not instance.id:
+ # 用户已被手动删除,instance.orgs 时会使用 id 进行查找报错,所以判断不存在id时不做处理
+ return
if not created:
return
if instance.orgs.count() > 0:
diff --git a/apps/perms/api/application/application_permission.py b/apps/perms/api/application/application_permission.py
new file mode 100644
index 000000000..bd8fb3452
--- /dev/null
+++ b/apps/perms/api/application/application_permission.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+#
+from rest_framework.response import Response
+from rest_framework.generics import RetrieveAPIView
+
+from perms import serializers
+from perms.models import ApplicationPermission
+from applications.models import Application
+from common.permissions import IsValidUser
+from ..base import BasePermissionViewSet
+
+
+class ApplicationPermissionViewSet(BasePermissionViewSet):
+ """
+ 应用授权列表的增删改查API
+ """
+ model = ApplicationPermission
+ serializer_class = serializers.ApplicationPermissionSerializer
+ filterset_fields = {
+ 'name': ['exact'],
+ 'category': ['exact'],
+ 'type': ['exact', 'in'],
+ 'from_ticket': ['exact']
+ }
+ search_fields = ['name', 'category', 'type']
+ custom_filter_fields = BasePermissionViewSet.custom_filter_fields + [
+ 'application_id', 'application', 'app', 'app_name'
+ ]
+ ordering_fields = ('name',)
+ ordering = ('name',)
+
+ def get_queryset(self):
+ queryset = super().get_queryset().prefetch_related(
+ "applications", "users", "user_groups", "system_users"
+ )
+ return queryset
+
+ def filter_application(self, queryset):
+ app_id = self.request.query_params.get('application_id') or \
+ self.request.query_params.get('app')
+ app_name = self.request.query_params.get('application') or \
+ self.request.query_params.get('app_name')
+
+ if app_id:
+ applications = Application.objects.filter(pk=app_id)
+ elif app_name:
+ applications = Application.objects.filter(name=app_name)
+ else:
+ return queryset
+ if not applications:
+ return queryset.none()
+ queryset = queryset.filter(applications__in=applications)
+ return queryset
+
+ def filter_queryset(self, queryset):
+ queryset = super().filter_queryset(queryset)
+ queryset = self.filter_application(queryset)
+ return queryset
+
+
+class ApplicationPermissionActionsApi(RetrieveAPIView):
+ permission_classes = (IsValidUser,)
+
+ def retrieve(self, request, *args, **kwargs):
+ category = request.GET.get('category')
+ actions = ApplicationPermission.get_include_actions_choices(category=category)
+ return Response(data=actions)
diff --git a/apps/perms/api/user_permission/assets/mixin.py b/apps/perms/api/user_permission/assets/mixin.py
index 6df224bee..743881eba 100644
--- a/apps/perms/api/user_permission/assets/mixin.py
+++ b/apps/perms/api/user_permission/assets/mixin.py
@@ -33,7 +33,9 @@ class UserAllGrantedAssetsQuerysetMixin:
only_fields = serializers.AssetGrantedSerializer.Meta.only_fields
pagination_class = AllGrantedAssetPagination
user: User
-
+ ordering_fields = ("hostname", "ip", "port", "cpu_cores")
+ ordering = ('hostname', )
+
def get_queryset(self):
if getattr(self, 'swagger_fake_view', False):
return Asset.objects.none()
diff --git a/apps/perms/notifications.py b/apps/perms/notifications.py
index 28e315ab3..c6917dcbd 100644
--- a/apps/perms/notifications.py
+++ b/apps/perms/notifications.py
@@ -1,4 +1,7 @@
+<<<<<<< HEAD
+=======
+>>>>>>> origin
from django.utils.translation import ugettext as _
from django.template.loader import render_to_string
@@ -10,7 +13,7 @@ class PermedAssetsWillExpireUserMsg(UserMessage):
def __init__(self, user, assets, day_count=0):
super().__init__(user)
self.assets = assets
- self.day_count = day_count
+ self.day_count = _('today') if day_count == 0 else day_count
def get_html_msg(self) -> dict:
subject = _("You permed assets is about to expire")
@@ -42,7 +45,7 @@ class AssetPermsWillExpireForOrgAdminMsg(UserMessage):
super().__init__(user)
self.perms = perms
self.org = org
- self.day_count = day_count
+ self.day_count = _('today') if day_count == 0 else day_count
def get_items_with_url(self):
items_with_url = []
@@ -50,7 +53,7 @@ class AssetPermsWillExpireForOrgAdminMsg(UserMessage):
url = js_reverse(
'perms:asset-permission-detail',
kwargs={'pk': perm.id}, external=True,
- api_to_ui=True
+ api_to_ui=True, is_console=True
) + f'?oid={perm.org_id}'
items_with_url.append([perm.name, url])
return items_with_url
@@ -60,7 +63,7 @@ class AssetPermsWillExpireForOrgAdminMsg(UserMessage):
subject = _("Asset permissions is about to expire")
context = {
'name': self.user.name,
- 'count': self.day_count,
+ 'count': str(self.day_count),
'items_with_url': items_with_url,
'item_type': _('asset permissions of organization {}').format(self.org)
}
@@ -80,3 +83,81 @@ class AssetPermsWillExpireForOrgAdminMsg(UserMessage):
perms = AssetPermission.objects.all()[:10]
org = Organization.objects.first()
return cls(user, perms, org)
+<<<<<<< HEAD
+=======
+
+
+class PermedAppsWillExpireUserMsg(UserMessage):
+ def __init__(self, user, apps, day_count=0):
+ super().__init__(user)
+ self.apps = apps
+ self.day_count = _('today') if day_count == 0 else day_count
+
+ def get_html_msg(self) -> dict:
+ subject = _("Your permed applications is about to expire")
+ context = {
+ 'name': self.user.name,
+ 'count': str(self.day_count),
+ 'item_type': _('permed applications'),
+ 'items': [str(app) for app in self.apps]
+ }
+ message = render_to_string('perms/_msg_permed_items_expire.html', context)
+ return {
+ 'subject': subject,
+ 'message': message
+ }
+
+ @classmethod
+ def gen_test_msg(cls):
+ from users.models import User
+ from applications.models import Application
+
+ user = User.objects.first()
+ apps = Application.objects.all()[:10]
+ return cls(user, apps)
+
+
+class AppPermsWillExpireForOrgAdminMsg(UserMessage):
+ def __init__(self, user, perms, org, day_count=0):
+ super().__init__(user)
+ self.perms = perms
+ self.org = org
+ self.day_count = _('today') if day_count == 0 else day_count
+
+ def get_items_with_url(self):
+ items_with_url = []
+ for perm in self.perms:
+ url = js_reverse(
+ 'perms:application-permission-detail',
+ kwargs={'pk': perm.id}, external=True,
+ api_to_ui=True, is_console=True
+ ) + f'?oid={perm.org_id}'
+ items_with_url.append([perm.name, url])
+ return items_with_url
+
+ def get_html_msg(self) -> dict:
+ items = self.get_items_with_url()
+ subject = _('Application permissions is about to expire')
+ context = {
+ 'name': self.user.name,
+ 'count': str(self.day_count),
+ 'item_type': _('application permissions of organization {}').format(self.org),
+ 'items_with_url': items
+ }
+ message = render_to_string('perms/_msg_item_permissions_expire.html', context)
+ return {
+ 'subject': subject,
+ 'message': message
+ }
+
+ @classmethod
+ def gen_test_msg(cls):
+ from users.models import User
+ from perms.models import ApplicationPermission
+ from orgs.models import Organization
+
+ user = User.objects.first()
+ perms = ApplicationPermission.objects.all()[:10]
+ org = Organization.objects.first()
+ return cls(user, perms, org)
+>>>>>>> origin
diff --git a/apps/perms/tasks.py b/apps/perms/tasks.py
index b5de6d03e..58cbc66d6 100644
--- a/apps/perms/tasks.py
+++ b/apps/perms/tasks.py
@@ -72,7 +72,7 @@ def check_asset_permission_will_expired():
for asset_perm in asset_perms:
date_expired = dt_parser(asset_perm.date_expired)
- remain_days = (end - date_expired).days
+ remain_days = (date_expired - start).days
org = asset_perm.org
# 资产授权按照组织分类
@@ -100,3 +100,51 @@ def check_asset_permission_will_expired():
org_admins = org.admins.all()
for org_admin in org_admins:
AssetPermsWillExpireForOrgAdminMsg(org_admin, perms, org, day_count).publish_async()
+<<<<<<< HEAD
+=======
+
+
+@register_as_period_task(crontab='0 10 * * *')
+@shared_task()
+@atomic()
+@tmp_to_root_org()
+def check_app_permission_will_expired():
+ start = local_now()
+ end = start + timedelta(days=3)
+
+ app_perms = ApplicationPermission.objects.filter(
+ date_expired__gte=start,
+ date_expired__lte=end
+ ).distinct()
+
+ user_app_remain_day_mapper = defaultdict(dict)
+ org_perm_remain_day_mapper = defaultdict(dict)
+
+ for app_perm in app_perms:
+ date_expired = dt_parser(app_perm.date_expired)
+ remain_days = (date_expired - start).days
+
+ org = app_perm.org
+ if org in org_perm_remain_day_mapper[remain_days]:
+ org_perm_remain_day_mapper[remain_days][org].add(app_perm)
+ else:
+ org_perm_remain_day_mapper[remain_days][org] = {app_perm, }
+
+ users = app_perm.get_all_users()
+ apps = app_perm.applications.all()
+ for u in users:
+ if u in user_app_remain_day_mapper[remain_days]:
+ user_app_remain_day_mapper[remain_days][u].update(apps)
+ else:
+ user_app_remain_day_mapper[remain_days][u] = set(apps)
+
+ for day_count, user_app_mapper in user_app_remain_day_mapper.items():
+ for user, apps in user_app_mapper.items():
+ PermedAppsWillExpireUserMsg(user, apps, day_count).publish_async()
+
+ for day_count, org_perm_mapper in org_perm_remain_day_mapper.items():
+ for org, perms in org_perm_mapper.items():
+ org_admins = org.admins.all()
+ for org_admin in org_admins:
+ AppPermsWillExpireForOrgAdminMsg(org_admin, perms, org, day_count).publish_async()
+>>>>>>> origin
diff --git a/apps/perms/urls/application_permission.py b/apps/perms/urls/application_permission.py
new file mode 100644
index 000000000..50772a8d5
--- /dev/null
+++ b/apps/perms/urls/application_permission.py
@@ -0,0 +1,50 @@
+# coding: utf-8
+#
+
+from django.urls import path, include
+from rest_framework_bulk.routes import BulkRouter
+from .. import api
+
+
+router = BulkRouter()
+router.register('application-permissions', api.ApplicationPermissionViewSet, 'application-permission')
+router.register('application-permissions-users-relations', api.ApplicationPermissionUserRelationViewSet, 'application-permissions-users-relation')
+router.register('application-permissions-user-groups-relations', api.ApplicationPermissionUserGroupRelationViewSet, 'application-permissions-user-groups-relation')
+router.register('application-permissions-applications-relations', api.ApplicationPermissionApplicationRelationViewSet, 'application-permissions-application-relation')
+router.register('application-permissions-system-users-relations', api.ApplicationPermissionSystemUserRelationViewSet, 'application-permissions-system-users-relation')
+
+user_permission_urlpatterns = [
+ path('/applications/', api.UserAllGrantedApplicationsApi.as_view(), name='user-applications'),
+ path('applications/', api.MyAllGrantedApplicationsApi.as_view(), name='my-applications'),
+
+ # Application As Tree
+ path('/applications/tree/', api.UserAllGrantedApplicationsAsTreeApi.as_view(), name='user-applications-as-tree'),
+ path('applications/tree/', api.MyAllGrantedApplicationsAsTreeApi.as_view(), name='my-applications-as-tree'),
+
+ # Application System Users
+ path('/applications//system-users/', api.UserGrantedApplicationSystemUsersApi.as_view(), name='user-application-system-users'),
+ path('applications//system-users/', api.MyGrantedApplicationSystemUsersApi.as_view(), name='my-application-system-users'),
+]
+
+user_group_permission_urlpatterns = [
+ path('/applications/', api.UserGroupGrantedApplicationsApi.as_view(), name='user-group-applications'),
+]
+
+permission_urlpatterns = [
+ # 授权规则中授权的用户和应用
+ path('/applications/all/', api.ApplicationPermissionAllApplicationListApi.as_view(), name='application-permission-all-applications'),
+ path('/users/all/', api.ApplicationPermissionAllUserListApi.as_view(), name='application-permission-all-users'),
+
+ # 验证用户是否有某个应用的权限
+ path('user/validate/', api.ValidateUserApplicationPermissionApi.as_view(), name='validate-user-application-permission'),
+
+ path('applications/actions/', api.ApplicationPermissionActionsApi.as_view(), name='application-actions'),
+]
+
+application_permission_urlpatterns = [
+ path('users/', include(user_permission_urlpatterns)),
+ path('user-groups/', include(user_group_permission_urlpatterns)),
+ path('application-permissions/', include(permission_urlpatterns))
+]
+
+application_permission_urlpatterns += router.urls
diff --git a/apps/rbac/builtin.py b/apps/rbac/builtin.py
index 34733dc59..c56326601 100644
--- a/apps/rbac/builtin.py
+++ b/apps/rbac/builtin.py
@@ -5,8 +5,10 @@ from .const import Scope, system_exclude_permissions, org_exclude_permissions
_view_root_perms = (
('orgs', 'organization', 'view', 'rootorg'),
)
+_view_all_joined_org_perms = (
+ ('orgs', 'organization', 'view', 'alljoinedorg'),
+)
-# 工作台也区分组织后再考虑
user_perms = (
('rbac', 'menupermission', 'view', 'workbench'),
('rbac', 'menupermission', 'view', 'webterminal'),
@@ -21,11 +23,11 @@ user_perms = (
)
system_user_perms = (
- ('authentication', 'connectiontoken', 'add', 'connectiontoken'),
+ ('authentication', 'connectiontoken', 'add,view', 'connectiontoken'),
('authentication', 'temptoken', 'add,change,view', 'temptoken'),
('authentication', 'accesskey', '*', '*'),
('tickets', 'ticket', 'view', 'ticket'),
-) + user_perms
+) + user_perms + _view_all_joined_org_perms
_auditor_perms = (
('rbac', 'menupermission', 'view', 'audit'),
diff --git a/apps/rbac/const.py b/apps/rbac/const.py
index 20038de19..f8789a110 100644
--- a/apps/rbac/const.py
+++ b/apps/rbac/const.py
@@ -40,6 +40,10 @@ exclude_permissions = (
('assets', 'gathereduser', 'add,delete,change', 'gathereduser'),
('assets', 'accountbackupplanexecution', 'delete,change', 'accountbackupplanexecution'),
('assets', 'authbook', 'change', 'authbook'),
+ # TODO 暂时去掉历史账号的权限
+ ('assets', 'authbook', '*', 'assethistoryaccount'),
+ ('assets', 'authbook', '*', 'assethistoryaccountsecret'),
+
('perms', 'userassetgrantedtreenoderelation', '*', '*'),
('perms', 'usergrantedmappingnode', '*', '*'),
('perms', 'permnode', '*', '*'),
diff --git a/apps/rbac/models/permission.py b/apps/rbac/models/permission.py
index bc8fa6231..5b98b6045 100644
--- a/apps/rbac/models/permission.py
+++ b/apps/rbac/models/permission.py
@@ -60,11 +60,11 @@ class Permission(DjangoPermission):
if actions == '*' and resource == '*':
pass
elif actions == '*' and resource != '*':
- kwargs['codename__iregex'] = r'[a-z]+_{}'.format(resource)
+ kwargs['codename__iregex'] = r'[a-z]+_{}$'.format(resource)
elif actions != '*' and resource == '*':
kwargs['codename__iregex'] = r'({})_[a-z]+'.format(actions_regex)
else:
- kwargs['codename__iregex'] = r'({})_{}'.format(actions_regex, resource)
+ kwargs['codename__iregex'] = r'({})_{}$'.format(actions_regex, resource)
q |= Q(**kwargs)
return q
diff --git a/apps/rbac/models/rolebinding.py b/apps/rbac/models/rolebinding.py
index 9f5dcd3e3..b9fe3c10b 100644
--- a/apps/rbac/models/rolebinding.py
+++ b/apps/rbac/models/rolebinding.py
@@ -126,9 +126,16 @@ class RoleBinding(JMSBaseModel):
org_ids = [b.org.id for b in bindings if b.org]
orgs = all_orgs.filter(id__in=org_ids)
+ workbench_perm = 'rbac.view_workbench'
# 全局组织
- if orgs and perm != 'rbac.view_workbench' and user.has_perm('orgs.view_rootorg'):
- orgs = [Organization.root(), *list(orgs)]
+ if orgs and perm != workbench_perm and user.has_perm('orgs.view_rootorg'):
+ root_org = Organization.root()
+ orgs = [root_org, *list(orgs)]
+ elif orgs and perm == workbench_perm and user.has_perm('orgs.view_alljoinedorg'):
+ # Todo: 先复用组织
+ root_org = Organization.root()
+ root_org.name = _("All organizations")
+ orgs = [root_org, *list(orgs)]
return orgs
diff --git a/apps/settings/api/__init__.py b/apps/settings/api/__init__.py
index 65438dda1..176a6c2c6 100644
--- a/apps/settings/api/__init__.py
+++ b/apps/settings/api/__init__.py
@@ -5,6 +5,4 @@ from .dingtalk import *
from .feishu import *
from .public import *
from .email import *
-from .alibaba_sms import *
-from .tencent_sms import *
from .sms import *
diff --git a/apps/settings/api/alibaba_sms.py b/apps/settings/api/alibaba_sms.py
deleted file mode 100644
index 8240ba0e0..000000000
--- a/apps/settings/api/alibaba_sms.py
+++ /dev/null
@@ -1,58 +0,0 @@
-from rest_framework.views import Response
-from rest_framework.generics import GenericAPIView
-from rest_framework.exceptions import APIException
-from rest_framework import status
-from django.utils.translation import gettext_lazy as _
-
-from common.sdk.sms.alibaba import AlibabaSMS
-from settings.models import Setting
-from common.exceptions import JMSException
-
-from .. import serializers
-
-
-class AlibabaSMSTestingAPI(GenericAPIView):
- serializer_class = serializers.AlibabaSMSSettingSerializer
- rbac_perms = {
- 'POST': 'settings.change_sms'
- }
-
- def post(self, request):
- serializer = self.serializer_class(data=request.data)
- serializer.is_valid(raise_exception=True)
-
- alibaba_access_key_id = serializer.validated_data['ALIBABA_ACCESS_KEY_ID']
- alibaba_access_key_secret = serializer.validated_data.get('ALIBABA_ACCESS_KEY_SECRET')
- alibaba_verify_sign_name = serializer.validated_data['ALIBABA_VERIFY_SIGN_NAME']
- alibaba_verify_template_code = serializer.validated_data['ALIBABA_VERIFY_TEMPLATE_CODE']
- test_phone = serializer.validated_data.get('SMS_TEST_PHONE')
-
- if not test_phone:
- raise JMSException(code='test_phone_required', detail=_('test_phone is required'))
-
- if not alibaba_access_key_secret:
- secret = Setting.objects.filter(name='ALIBABA_ACCESS_KEY_SECRET').first()
- if secret:
- alibaba_access_key_secret = secret.cleaned_value
-
- alibaba_access_key_secret = alibaba_access_key_secret or ''
-
- try:
- client = AlibabaSMS(
- access_key_id=alibaba_access_key_id,
- access_key_secret=alibaba_access_key_secret
- )
-
- client.send_sms(
- phone_numbers=[test_phone],
- sign_name=alibaba_verify_sign_name,
- template_code=alibaba_verify_template_code,
- template_param={'code': 'test'}
- )
- return Response(status=status.HTTP_200_OK, data={'msg': _('Test success')})
- except APIException as e:
- try:
- error = e.detail['errmsg']
- except:
- error = e.detail
- return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': error})
diff --git a/apps/settings/api/public.py b/apps/settings/api/public.py
index 1351fd6cd..161b8002a 100644
--- a/apps/settings/api/public.py
+++ b/apps/settings/api/public.py
@@ -3,7 +3,11 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
from django.conf import settings
from jumpserver.utils import has_valid_xpack_license, get_xpack_license_info
-from common.utils import get_logger, lazyproperty
+from common.utils import get_logger, lazyproperty, get_object_or_none
+from authentication.models import ConnectionToken
+from orgs.utils import tmp_to_root_org
+from common.permissions import IsValidUserOrConnectionToken
+
from .. import serializers
from ..utils import get_interface_setting_or_default
@@ -28,7 +32,7 @@ class OpenPublicSettingApi(generics.RetrieveAPIView):
class PublicSettingApi(OpenPublicSettingApi):
- permission_classes = (IsAuthenticated,)
+ permission_classes = (IsValidUserOrConnectionToken,)
serializer_class = serializers.PrivateSettingSerializer
def get_object(self):
diff --git a/apps/settings/api/settings.py b/apps/settings/api/settings.py
index 3e1d9336d..0f487c280 100644
--- a/apps/settings/api/settings.py
+++ b/apps/settings/api/settings.py
@@ -34,11 +34,13 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
'cas': serializers.CASSettingSerializer,
'sso': serializers.SSOSettingSerializer,
'saml2': serializers.SAML2SettingSerializer,
+ 'oauth2': serializers.OAuth2SettingSerializer,
'clean': serializers.CleaningSerializer,
'other': serializers.OtherSettingSerializer,
'sms': serializers.SMSSettingSerializer,
'alibaba': serializers.AlibabaSMSSettingSerializer,
'tencent': serializers.TencentSMSSettingSerializer,
+ 'cmpp2': serializers.CMPP2SMSSettingSerializer,
}
rbac_category_permissions = {
@@ -113,9 +115,12 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
return data
def perform_update(self, serializer):
+ post_data_names = list(self.request.data.keys())
settings_items = self.parse_serializer_data(serializer)
serializer_data = getattr(serializer, 'data', {})
for item in settings_items:
+ if item['name'] not in post_data_names:
+ continue
changed, setting = Setting.update_or_create(**item)
if not changed:
continue
diff --git a/apps/settings/api/sms.py b/apps/settings/api/sms.py
index bb30fa3aa..668b5ec56 100644
--- a/apps/settings/api/sms.py
+++ b/apps/settings/api/sms.py
@@ -1,8 +1,19 @@
-from rest_framework.generics import ListAPIView
+import importlib
+
+from collections import OrderedDict
+
+from rest_framework.generics import ListAPIView, GenericAPIView
from rest_framework.response import Response
+from rest_framework.exceptions import APIException
+from rest_framework import status
+from django.utils.translation import gettext_lazy as _
from common.sdk.sms import BACKENDS
+from common.exceptions import JMSException
from settings.serializers.sms import SMSBackendSerializer
+from settings.models import Setting
+
+from .. import serializers
class SMSBackendAPI(ListAPIView):
@@ -21,3 +32,111 @@ class SMSBackendAPI(ListAPIView):
]
return Response(data)
+
+
+class SMSTestingAPI(GenericAPIView):
+ backends_serializer = {
+ 'alibaba': serializers.AlibabaSMSSettingSerializer,
+ 'tencent': serializers.TencentSMSSettingSerializer,
+ 'cmpp2': serializers.CMPP2SMSSettingSerializer
+ }
+ rbac_perms = {
+ 'POST': 'settings.change_sms'
+ }
+
+ @staticmethod
+ def get_or_from_setting(key, value=''):
+ if not value:
+ secret = Setting.objects.filter(name=key).first()
+ if secret:
+ value = secret.cleaned_value
+
+ return value or ''
+
+ def get_alibaba_params(self, data):
+ init_params = {
+ 'access_key_id': data['ALIBABA_ACCESS_KEY_ID'],
+ 'access_key_secret': self.get_or_from_setting(
+ 'ALIBABA_ACCESS_KEY_SECRET', data.get('ALIBABA_ACCESS_KEY_SECRET')
+ )
+ }
+ send_sms_params = {
+ 'sign_name': data['ALIBABA_VERIFY_SIGN_NAME'],
+ 'template_code': data['ALIBABA_VERIFY_TEMPLATE_CODE'],
+ 'template_param': {'code': '666666'}
+ }
+ return init_params, send_sms_params
+
+ def get_tencent_params(self, data):
+ init_params = {
+ 'secret_id': data['TENCENT_SECRET_ID'],
+ 'secret_key': self.get_or_from_setting(
+ 'TENCENT_SECRET_KEY', data.get('TENCENT_SECRET_KEY')
+ ),
+ 'sdkappid': data['TENCENT_SDKAPPID']
+ }
+ send_sms_params = {
+ 'sign_name': data['TENCENT_VERIFY_SIGN_NAME'],
+ 'template_code': data['TENCENT_VERIFY_TEMPLATE_CODE'],
+ 'template_param': OrderedDict(code='666666')
+ }
+ return init_params, send_sms_params
+
+ def get_cmpp2_params(self, data):
+ init_params = {
+ 'host': data['CMPP2_HOST'], 'port': data['CMPP2_PORT'],
+ 'sp_id': data['CMPP2_SP_ID'], 'src_id': data['CMPP2_SRC_ID'],
+ 'sp_secret': self.get_or_from_setting(
+ 'CMPP2_SP_SECRET', data.get('CMPP2_SP_SECRET')
+ ),
+ 'service_id': data['CMPP2_SERVICE_ID'],
+ }
+ send_sms_params = {
+ 'sign_name': data['CMPP2_VERIFY_SIGN_NAME'],
+ 'template_code': data['CMPP2_VERIFY_TEMPLATE_CODE'],
+ 'template_param': OrderedDict(code='666666')
+ }
+ return init_params, send_sms_params
+
+ def get_params_by_backend(self, backend, data):
+ """
+ 返回两部分参数
+ 1、实例化参数
+ 2、发送测试短信参数
+ """
+ get_params_func = getattr(self, 'get_%s_params' % backend)
+ return get_params_func(data)
+
+ def post(self, request, backend):
+ serializer_class = self.backends_serializer.get(backend)
+ if serializer_class is None:
+ raise JMSException(_('Invalid SMS platform'))
+ serializer = serializer_class(data=request.data)
+ serializer.is_valid(raise_exception=True)
+
+ test_phone = serializer.validated_data.get('SMS_TEST_PHONE')
+ if not test_phone:
+ raise JMSException(code='test_phone_required', detail=_('test_phone is required'))
+
+ init_params, send_sms_params = self.get_params_by_backend(backend, serializer.validated_data)
+
+ m = importlib.import_module(f'common.sdk.sms.{backend}', __package__)
+ try:
+ client = m.client(**init_params)
+ client.send_sms(
+ phone_numbers=[test_phone],
+ **send_sms_params
+ )
+ status_code = status.HTTP_200_OK
+ data = {'msg': _('Test success')}
+ except APIException as e:
+ try:
+ error = e.detail['errmsg']
+ except:
+ error = e.detail
+ status_code = status.HTTP_400_BAD_REQUEST
+ data = {'error': error}
+ except Exception as e:
+ status_code = status.HTTP_400_BAD_REQUEST
+ data = {'error': str(e)}
+ return Response(status=status_code, data=data)
diff --git a/apps/settings/api/tencent_sms.py b/apps/settings/api/tencent_sms.py
deleted file mode 100644
index 83a87a474..000000000
--- a/apps/settings/api/tencent_sms.py
+++ /dev/null
@@ -1,63 +0,0 @@
-from collections import OrderedDict
-
-from rest_framework.views import Response
-from rest_framework.generics import GenericAPIView
-from rest_framework.exceptions import APIException
-from rest_framework import status
-from django.utils.translation import gettext_lazy as _
-
-from common.sdk.sms.tencent import TencentSMS
-from settings.models import Setting
-from common.exceptions import JMSException
-
-from .. import serializers
-
-
-class TencentSMSTestingAPI(GenericAPIView):
- serializer_class = serializers.TencentSMSSettingSerializer
- rbac_perms = {
- 'POST': 'settings.change_sms'
- }
-
- def post(self, request):
- serializer = self.serializer_class(data=request.data)
- serializer.is_valid(raise_exception=True)
-
- tencent_secret_id = serializer.validated_data['TENCENT_SECRET_ID']
- tencent_secret_key = serializer.validated_data.get('TENCENT_SECRET_KEY')
- tencent_verify_sign_name = serializer.validated_data['TENCENT_VERIFY_SIGN_NAME']
- tencent_verify_template_code = serializer.validated_data['TENCENT_VERIFY_TEMPLATE_CODE']
- tencent_sdkappid = serializer.validated_data.get('TENCENT_SDKAPPID')
-
- test_phone = serializer.validated_data.get('SMS_TEST_PHONE')
-
- if not test_phone:
- raise JMSException(code='test_phone_required', detail=_('test_phone is required'))
-
- if not tencent_secret_key:
- secret = Setting.objects.filter(name='TENCENT_SECRET_KEY').first()
- if secret:
- tencent_secret_key = secret.cleaned_value
-
- tencent_secret_key = tencent_secret_key or ''
-
- try:
- client = TencentSMS(
- secret_id=tencent_secret_id,
- secret_key=tencent_secret_key,
- sdkappid=tencent_sdkappid
- )
-
- client.send_sms(
- phone_numbers=[test_phone],
- sign_name=tencent_verify_sign_name,
- template_code=tencent_verify_template_code,
- template_param=OrderedDict(code='666666')
- )
- return Response(status=status.HTTP_200_OK, data={'msg': _('Test success')})
- except APIException as e:
- try:
- error = e.detail['errmsg']
- except:
- error = e.detail
- return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': error})
diff --git a/apps/settings/models.py b/apps/settings/models.py
index 4546f9624..8b91765cf 100644
--- a/apps/settings/models.py
+++ b/apps/settings/models.py
@@ -1,9 +1,13 @@
+import os
import json
from django.db import models
from django.db.utils import ProgrammingError, OperationalError
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
+from django.core.files.storage import default_storage
+from django.core.files.base import ContentFile
+from django.core.files.uploadedfile import InMemoryUploadedFile
from common.utils import signer, get_logger
@@ -118,6 +122,14 @@ class Setting(models.Model):
setattr(settings, key, value)
self.__class__.update_or_create(key, value, encrypted=False, category=self.category)
+ @classmethod
+ def save_to_file(cls, value: InMemoryUploadedFile):
+ filename = value.name
+ filepath = f'settings/{filename}'
+ path = default_storage.save(filepath, ContentFile(value.read()))
+ url = default_storage.url(path)
+ return url
+
@classmethod
def update_or_create(cls, name='', value='', encrypted=False, category=''):
"""
@@ -128,6 +140,10 @@ class Setting(models.Model):
changed = False
if not setting:
setting = Setting(name=name, encrypted=encrypted, category=category)
+
+ if isinstance(value, InMemoryUploadedFile):
+ value = cls.save_to_file(value)
+
if setting.cleaned_value != value:
setting.encrypted = encrypted
setting.cleaned_value = value
diff --git a/apps/settings/serializers/auth/__init__.py b/apps/settings/serializers/auth/__init__.py
index c675f4070..1f9f360f9 100644
--- a/apps/settings/serializers/auth/__init__.py
+++ b/apps/settings/serializers/auth/__init__.py
@@ -9,3 +9,4 @@ from .sso import *
from .base import *
from .sms import *
from .saml2 import *
+from .oauth2 import *
diff --git a/apps/settings/serializers/auth/oauth2.py b/apps/settings/serializers/auth/oauth2.py
new file mode 100644
index 000000000..279f3c34f
--- /dev/null
+++ b/apps/settings/serializers/auth/oauth2.py
@@ -0,0 +1,55 @@
+
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import serializers
+
+from common.drf.fields import EncryptedField
+from common.utils import static_or_direct
+
+__all__ = [
+ 'OAuth2SettingSerializer',
+]
+
+
+class SettingImageField(serializers.ImageField):
+ def to_representation(self, value):
+ return static_or_direct(value)
+
+
+class OAuth2SettingSerializer(serializers.Serializer):
+ AUTH_OAUTH2 = serializers.BooleanField(
+ default=False, label=_('Enable OAuth2 Auth')
+ )
+ AUTH_OAUTH2_LOGO_PATH = SettingImageField(
+ allow_null=True, required=False, label=_('Logo')
+ )
+ AUTH_OAUTH2_PROVIDER = serializers.CharField(
+ required=True, max_length=16, label=_('Service provider')
+ )
+ AUTH_OAUTH2_CLIENT_ID = serializers.CharField(
+ required=True, max_length=1024, label=_('Client Id')
+ )
+ AUTH_OAUTH2_CLIENT_SECRET = EncryptedField(
+ required=False, max_length=1024, label=_('Client Secret')
+ )
+ AUTH_OAUTH2_SCOPE = serializers.CharField(
+ required=True, max_length=1024, label=_('Scope'), allow_blank=True
+ )
+ AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT = serializers.CharField(
+ required=True, max_length=1024, label=_('Provider auth endpoint')
+ )
+ AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT = serializers.CharField(
+ required=True, max_length=1024, label=_('Provider token endpoint')
+ )
+ AUTH_OAUTH2_ACCESS_TOKEN_METHOD = serializers.ChoiceField(
+ default='GET', label=_('Client authentication method'),
+ choices=(('GET', 'GET'), ('POST', 'POST'))
+ )
+ AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT = serializers.CharField(
+ required=True, max_length=1024, label=_('Provider userinfo endpoint')
+ )
+ AUTH_OAUTH2_USER_ATTR_MAP = serializers.DictField(
+ required=True, label=_('User attr map')
+ )
+ AUTH_OAUTH2_ALWAYS_UPDATE_USER = serializers.BooleanField(
+ default=True, label=_('Always update user')
+ )
diff --git a/apps/settings/serializers/auth/sms.py b/apps/settings/serializers/auth/sms.py
index cd3bef74c..1278dea06 100644
--- a/apps/settings/serializers/auth/sms.py
+++ b/apps/settings/serializers/auth/sms.py
@@ -2,15 +2,19 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from common.drf.fields import EncryptedField
+from common.validators import PhoneValidator
from common.sdk.sms import BACKENDS
-__all__ = ['SMSSettingSerializer', 'AlibabaSMSSettingSerializer', 'TencentSMSSettingSerializer']
+__all__ = [
+ 'SMSSettingSerializer', 'AlibabaSMSSettingSerializer', 'TencentSMSSettingSerializer',
+ 'CMPP2SMSSettingSerializer'
+]
class SMSSettingSerializer(serializers.Serializer):
SMS_ENABLED = serializers.BooleanField(default=False, label=_('Enable SMS'))
SMS_BACKEND = serializers.ChoiceField(
- choices=BACKENDS.choices, default=BACKENDS.ALIBABA, label=_('SMS provider')
+ choices=BACKENDS.choices, default=BACKENDS.ALIBABA, label=_('SMS provider / Protocol')
)
@@ -20,7 +24,10 @@ class SignTmplPairSerializer(serializers.Serializer):
class BaseSMSSettingSerializer(serializers.Serializer):
- SMS_TEST_PHONE = serializers.CharField(max_length=256, required=False, allow_blank=True, label=_('Test phone'))
+ SMS_TEST_PHONE = serializers.CharField(
+ max_length=256, required=False, validators=[PhoneValidator(), ],
+ allow_blank=True, label=_('Test phone')
+ )
def to_representation(self, instance):
data = super().to_representation(instance)
@@ -43,3 +50,29 @@ class TencentSMSSettingSerializer(BaseSMSSettingSerializer):
TENCENT_SDKAPPID = serializers.CharField(max_length=256, required=True, label='SDK app id')
TENCENT_VERIFY_SIGN_NAME = serializers.CharField(max_length=256, required=True, label=_('Signature'))
TENCENT_VERIFY_TEMPLATE_CODE = serializers.CharField(max_length=256, required=True, label=_('Template code'))
+
+
+class CMPP2SMSSettingSerializer(BaseSMSSettingSerializer):
+ CMPP2_HOST = serializers.CharField(max_length=256, required=True, label=_('Host'))
+ CMPP2_PORT = serializers.IntegerField(default=7890, label=_('Port'))
+ CMPP2_SP_ID = serializers.CharField(max_length=128, required=True, label=_('Enterprise code(SP id)'))
+ CMPP2_SP_SECRET = EncryptedField(max_length=256, required=False, label=_('Shared secret(Shared secret)'))
+ CMPP2_SRC_ID = serializers.CharField(max_length=256, required=False, label=_('Original number(Src id)'))
+ CMPP2_SERVICE_ID = serializers.CharField(max_length=256, required=True, label=_('Business type(Service id)'))
+ CMPP2_VERIFY_SIGN_NAME = serializers.CharField(max_length=256, required=True, label=_('Signature'))
+ CMPP2_VERIFY_TEMPLATE_CODE = serializers.CharField(
+ max_length=69, required=True, label=_('Template'),
+ help_text=_('Template need contain {code} and Signature + template length does not exceed 67 words. '
+ 'For example, your verification code is {code}, which is valid for 5 minutes. '
+ 'Please do not disclose it to others.')
+ )
+
+ def validate(self, attrs):
+ sign_name = attrs.get('CMPP2_VERIFY_SIGN_NAME', '')
+ template_code = attrs.get('CMPP2_VERIFY_TEMPLATE_CODE', '')
+ if template_code.find('{code}') == -1:
+ raise serializers.ValidationError(_('The template needs to contain {code}'))
+ if len(sign_name + template_code) > 65:
+ # 保证验证码内容在一条短信中(长度小于70字), 签名两边的括号和空格占3个字,再减去2个即可(验证码占用4个但占位符6个
+ raise serializers.ValidationError(_('Signature + Template must not exceed 65 words'))
+ return attrs
diff --git a/apps/settings/serializers/auth/sso.py b/apps/settings/serializers/auth/sso.py
index c04f38920..113391b45 100644
--- a/apps/settings/serializers/auth/sso.py
+++ b/apps/settings/serializers/auth/sso.py
@@ -1,4 +1,3 @@
-
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
@@ -14,5 +13,5 @@ class SSOSettingSerializer(serializers.Serializer):
)
AUTH_SSO_AUTHKEY_TTL = serializers.IntegerField(
required=False, label=_('SSO auth key TTL'), help_text=_("Unit: second"),
- min_value=1, max_value=60*30
+ min_value=60, max_value=60 * 30
)
diff --git a/apps/settings/serializers/other.py b/apps/settings/serializers/other.py
index 7d701756c..775e26dd7 100644
--- a/apps/settings/serializers/other.py
+++ b/apps/settings/serializers/other.py
@@ -30,6 +30,11 @@ class OtherSettingSerializer(serializers.Serializer):
help_text=_("Perm single to ungroup node")
)
+ TICKET_AUTHORIZE_DEFAULT_TIME = serializers.IntegerField(
+ min_value=7, max_value=9999, required=False,
+ label=_("Ticket authorize default time"), help_text=_("Unit: day")
+ )
+
HELP_DOCUMENT_URL = serializers.URLField(
required=False, allow_blank=True, allow_null=True, label=_("Help Docs URL"),
help_text=_('default: http://docs.jumpserver.org')
diff --git a/apps/settings/serializers/public.py b/apps/settings/serializers/public.py
index 73b61a3bc..04e2b85af 100644
--- a/apps/settings/serializers/public.py
+++ b/apps/settings/serializers/public.py
@@ -14,6 +14,7 @@ class PublicSettingSerializer(serializers.Serializer):
class PrivateSettingSerializer(PublicSettingSerializer):
WINDOWS_SKIP_ALL_MANUAL_PASSWORD = serializers.BooleanField()
OLD_PASSWORD_HISTORY_LIMIT_COUNT = serializers.IntegerField()
+ TICKET_AUTHORIZE_DEFAULT_TIME = serializers.IntegerField()
SECURITY_MAX_IDLE_TIME = serializers.IntegerField()
SECURITY_VIEW_AUTH_NEED_MFA = serializers.BooleanField()
SECURITY_MFA_VERIFY_TTL = serializers.IntegerField()
diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py
index 7baa19196..3152a5ef5 100644
--- a/apps/settings/serializers/settings.py
+++ b/apps/settings/serializers/settings.py
@@ -7,7 +7,7 @@ from .auth import (
LDAPSettingSerializer, OIDCSettingSerializer, KeycloakSettingSerializer,
CASSettingSerializer, RadiusSettingSerializer, FeiShuSettingSerializer,
WeComSettingSerializer, DingTalkSettingSerializer, AlibabaSMSSettingSerializer,
- TencentSMSSettingSerializer,
+ TencentSMSSettingSerializer, CMPP2SMSSettingSerializer
)
from .terminal import TerminalSettingSerializer
from .security import SecuritySettingSerializer
@@ -37,6 +37,7 @@ class SettingsSerializer(
CleaningSerializer,
AlibabaSMSSettingSerializer,
TencentSMSSettingSerializer,
+ CMPP2SMSSettingSerializer,
):
# encrypt_fields 现在使用 write_only 来判断了
pass
diff --git a/apps/settings/urls/api_urls.py b/apps/settings/urls/api_urls.py
index 728baf0ae..ba04f2a4b 100644
--- a/apps/settings/urls/api_urls.py
+++ b/apps/settings/urls/api_urls.py
@@ -16,8 +16,7 @@ urlpatterns = [
path('wecom/testing/', api.WeComTestingAPI.as_view(), name='wecom-testing'),
path('dingtalk/testing/', api.DingTalkTestingAPI.as_view(), name='dingtalk-testing'),
path('feishu/testing/', api.FeiShuTestingAPI.as_view(), name='feishu-testing'),
- path('alibaba/testing/', api.AlibabaSMSTestingAPI.as_view(), name='alibaba-sms-testing'),
- path('tencent/testing/', api.TencentSMSTestingAPI.as_view(), name='tencent-sms-testing'),
+ path('sms//testing/', api.SMSTestingAPI.as_view(), name='sms-testing'),
path('sms/backend/', api.SMSBackendAPI.as_view(), name='sms-backend'),
path('setting/', api.SettingsApi.as_view(), name='settings-setting'),
diff --git a/apps/settings/urls/ws_urls.py b/apps/settings/urls/ws_urls.py
new file mode 100644
index 000000000..b1555c957
--- /dev/null
+++ b/apps/settings/urls/ws_urls.py
@@ -0,0 +1,9 @@
+from django.urls import path
+
+from .. import ws
+
+app_name = 'common'
+
+urlpatterns = [
+ path('ws/setting/tools/', ws.ToolsWebsocket.as_asgi(), name='setting-tools-ws'),
+]
diff --git a/apps/settings/utils/__init__.py b/apps/settings/utils/__init__.py
index e17c4e43c..0927bde18 100644
--- a/apps/settings/utils/__init__.py
+++ b/apps/settings/utils/__init__.py
@@ -3,3 +3,5 @@
from .ldap import *
from .common import *
+from .ping import *
+from .telnet import *
diff --git a/apps/settings/utils/ping.py b/apps/settings/utils/ping.py
new file mode 100644
index 000000000..409edc83a
--- /dev/null
+++ b/apps/settings/utils/ping.py
@@ -0,0 +1,154 @@
+# -*- coding: utf-8 -*-
+#
+
+import os
+import select
+import socket
+import struct
+import time
+
+# From /usr/include/linux/icmp.h; your milage may vary.
+ICMP_ECHO_REQUEST = 8 # Seems to be the same on Solaris.
+
+
+def checksum(source_string):
+ """
+ I'm not too confident that this is right but testing seems
+ to suggest that it gives the same answers as in_cksum in ping.c
+ """
+ sum = 0
+ count_to = int((len(source_string) / 2) * 2)
+ for count in range(0, count_to, 2):
+ this = source_string[count + 1] * 256 + source_string[count]
+ sum = sum + this
+ sum = sum & 0xffffffff # Necessary?
+
+ if count_to < len(source_string):
+ sum = sum + ord(source_string[len(source_string) - 1])
+ sum = sum & 0xffffffff # Necessary?
+
+ sum = (sum >> 16) + (sum & 0xffff)
+ sum = sum + (sum >> 16)
+ answer = ~sum
+ answer = answer & 0xffff
+
+ # Swap bytes. Bugger me if I know why.
+ answer = answer >> 8 | (answer << 8 & 0xff00)
+
+ return answer
+
+
+def receive_one_ping(my_socket, id, timeout):
+ """
+ Receive the ping from the socket.
+ """
+ time_left = timeout
+ while True:
+ started_select = time.time()
+ what_ready = select.select([my_socket], [], [], time_left)
+ how_long_in_select = time.time() - started_select
+ if not what_ready[0]: # Timeout
+ return
+
+ time_received = time.time()
+ received_packet, addr = my_socket.recvfrom(1024)
+ icmpHeader = received_packet[20:28]
+ type, code, checksum, packet_id, sequence = struct.unpack("bbHHh", icmpHeader)
+ if packet_id == id:
+ bytes = struct.calcsize("d")
+ time_sent = struct.unpack("d", received_packet[28: 28 + bytes])[0]
+ return time_received - time_sent
+
+ time_left = time_left - how_long_in_select
+ if time_left <= 0:
+ return
+
+
+def send_one_ping(my_socket, dest_addr, id, psize):
+ """
+ Send one ping to the given >dest_addr<.
+ """
+ dest_addr = socket.gethostbyname(dest_addr)
+
+ # Remove header size from packet size
+ # psize = psize - 8
+ # laixintao edit:
+ # Do not need to remove header here. From BSD ping man:
+ # The default is 56, which translates into 64 ICMP data
+ # bytes when combined with the 8 bytes of ICMP header data.
+
+ # Header is type (8), code (8), checksum (16), id (16), sequence (16)
+ my_checksum = 0
+
+ # Make a dummy heder with a 0 checksum.
+ header = struct.pack("bbHHh", ICMP_ECHO_REQUEST, 0, my_checksum, id, 1)
+ bytes = struct.calcsize("d")
+ data = (psize - bytes) * b"Q"
+ data = struct.pack("d", time.time()) + data
+
+ # Calculate the checksum on the data and the dummy header.
+ my_checksum = checksum(header + data)
+
+ # Now that we have the right checksum, we put that in. It's just easier
+ # to make up a new header than to stuff it into the dummy.
+ header = struct.pack(
+ "bbHHh", ICMP_ECHO_REQUEST, 0, socket.htons(my_checksum), id, 1
+ )
+ packet = header + data
+ my_socket.sendto(packet, (dest_addr, 1)) # Don't know about the 1
+
+
+def ping(dest_addr, timeout, psize, flag=0):
+ """
+ Returns either the delay (in seconds) or none on timeout.
+ """
+ icmp = socket.getprotobyname("icmp")
+ try:
+ if os.getuid() != 0:
+ my_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, icmp)
+ else:
+ my_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp)
+ except socket.error as e:
+ if e.errno == 1:
+ # Operation not permitted
+ msg = str(e)
+ raise socket.error(msg)
+ raise # raise the original error
+
+ process_pre = os.getpid() & 0xFF00
+ flag = flag & 0x00FF
+ my_id = process_pre | flag
+
+ send_one_ping(my_socket, dest_addr, my_id, psize)
+ delay = receive_one_ping(my_socket, my_id, timeout)
+
+ my_socket.close()
+ return delay
+
+
+def verbose_ping(dest_addr, timeout=2, count=5, psize=64):
+ """
+ Send `count' ping with `psize' size to `dest_addr' with
+ the given `timeout' and display the result.
+ """
+ for i in range(count):
+ print("ping %s with ..." % dest_addr, end="")
+ try:
+ delay = ping(dest_addr, timeout, psize)
+ except socket.gaierror as e:
+ print("failed. (socket error: '%s')" % str(e))
+ break
+
+ if delay is None:
+ print("failed. (timeout within %ssec.)" % timeout)
+ else:
+ delay = delay * 1000
+ print("get ping in %0.4fms" % delay)
+ print()
+
+
+if __name__ == "__main__":
+ verbose_ping("google.com")
+ verbose_ping("192.168.4.1")
+ verbose_ping("www.baidu.com")
+ verbose_ping("sssssss")
diff --git a/apps/settings/utils/telnet.py b/apps/settings/utils/telnet.py
new file mode 100644
index 000000000..9785b43ae
--- /dev/null
+++ b/apps/settings/utils/telnet.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+#
+import socket
+import telnetlib
+
+PROMPT_REGEX = r'[\<|\[](.*)[\>|\]]'
+
+
+def telnet(dest_addr, port_number=23, timeout=10):
+ try:
+ connection = telnetlib.Telnet(dest_addr, port_number, timeout)
+ except (ConnectionRefusedError, socket.timeout, socket.gaierror) as e:
+ return False, str(e)
+ expected_regexes = [bytes(PROMPT_REGEX, encoding='ascii')]
+ index, prompt_regex, output = connection.expect(expected_regexes, timeout=3)
+ return True, output.decode('ascii')
+
+
+if __name__ == "__main__":
+ print(telnet(dest_addr='1.1.1.1', port_number=2222))
+ print(telnet(dest_addr='baidu.com', port_number=80))
+ print(telnet(dest_addr='baidu.com', port_number=8080))
+ print(telnet(dest_addr='192.168.4.1', port_number=2222))
+ print(telnet(dest_addr='192.168.4.1', port_number=2223))
+ print(telnet(dest_addr='ssssss', port_number=-1))
diff --git a/apps/settings/ws.py b/apps/settings/ws.py
new file mode 100644
index 000000000..3455abe2b
--- /dev/null
+++ b/apps/settings/ws.py
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+#
+
+import json
+
+from channels.generic.websocket import JsonWebsocketConsumer
+
+from common.db.utils import close_old_connections
+from common.utils import get_logger
+from .utils import ping, telnet
+
+logger = get_logger(__name__)
+
+
+class ToolsWebsocket(JsonWebsocketConsumer):
+
+ def connect(self):
+ user = self.scope["user"]
+ if user.is_authenticated:
+ self.accept()
+ else:
+ self.close()
+
+ def imitate_ping(self, dest_addr, timeout=3, count=5, psize=64):
+ """
+ Send `count' ping with `psize' size to `dest_addr' with
+ the given `timeout' and display the result.
+ """
+ logger.info('receive request ping {}'.format(dest_addr))
+ self.send_json({'msg': 'Trying {0}...\r\n'.format(dest_addr)})
+ for i in range(count):
+ msg = 'ping {0} with ...{1}\r\n'
+ try:
+ delay = ping(dest_addr, timeout, psize)
+ except Exception as e:
+ msg = msg.format(dest_addr, 'failed. (socket error: {})'.format(str(e)))
+ logger.error(msg)
+ self.send_json({'msg': msg})
+ break
+ if delay is None:
+ msg = msg.format(dest_addr, 'failed. (timeout within {}sec.)'.format(timeout))
+ else:
+ delay = delay * 1000
+ msg = msg.format(dest_addr, 'get ping in %0.4fms' % delay)
+ self.send_json({'msg': msg})
+
+ def imitate_telnet(self, dest_addr, port_num=23, timeout=10):
+ logger.info('receive request telnet {}'.format(dest_addr))
+ self.send_json({'msg': 'Trying {0} {1}...\r\n'.format(dest_addr, port_num)})
+ msg = 'Telnet: {}'
+ try:
+ is_connective, resp = telnet(dest_addr, port_num, timeout)
+ if is_connective:
+ msg = msg.format('Connected to {0} {1}\r\n{2}'.format(dest_addr, port_num, resp))
+ else:
+ msg = msg.format('Connect to {0} {1} {2}\r\nTelnet: Unable to connect to remote host'
+ .format(dest_addr, port_num, resp))
+ except Exception as e:
+ logger.error(msg)
+ msg = msg.format(str(e))
+ finally:
+ self.send_json({'msg': msg})
+
+ def receive(self, text_data=None, bytes_data=None, **kwargs):
+ data = json.loads(text_data)
+ tool_type = data.get('tool_type', 'Ping')
+ dest_addr = data.get('dest_addr')
+ if tool_type == 'Ping':
+ self.imitate_ping(dest_addr)
+ else:
+ port_num = data.get('port_num')
+ self.imitate_telnet(dest_addr, port_num)
+ self.close()
+
+ def disconnect(self, code):
+ self.close()
+ close_old_connections()
diff --git a/apps/static/img/login_oauth2_logo.png b/apps/static/img/login_oauth2_logo.png
new file mode 100644
index 000000000..b1cf562b0
Binary files /dev/null and b/apps/static/img/login_oauth2_logo.png differ
diff --git a/apps/static/js/jumpserver.js b/apps/static/js/jumpserver.js
index e1e6707b2..c95c06c2c 100644
--- a/apps/static/js/jumpserver.js
+++ b/apps/static/js/jumpserver.js
@@ -270,10 +270,12 @@ function requestApi(props) {
if (typeof(dataBody) === "object") {
dataBody = JSON.stringify(dataBody)
}
+ var headers = props.headers || {}
$.ajax({
url: props.url,
type: props.method || "PATCH",
+ headers: headers,
data: dataBody,
contentType: props.content_type || "application/json; charset=utf-8",
dataType: props.data_type || "json"
@@ -1504,17 +1506,11 @@ function getStatusIcon(status, mapping, title) {
function fillKey(key) {
- let keySize = 128
- // 如果超过 key 16 位, 最大取 32 位,需要更改填充
- if (key.length > 16) {
- key = key.slice(0, 32)
- keySize = keySize * 2
+ const KeyLength = 16
+ if (key.length > KeyLength) {
+ key = key.slice(0, KeyLength)
}
- const filledKeyLength = keySize / 8
- if (key.length >= filledKeyLength) {
- return key.slice(0, filledKeyLength)
- }
- const filledKey = Buffer.alloc(keySize / 8)
+ const filledKey = Buffer.alloc(KeyLength)
const keys = Buffer.from(key)
for (let i = 0; i < keys.length; i++) {
filledKey[i] = keys[i]
diff --git a/apps/templates/resource_download.html b/apps/templates/resource_download.html
index 4b60e1e09..9938d60a4 100644
--- a/apps/templates/resource_download.html
+++ b/apps/templates/resource_download.html
@@ -15,7 +15,7 @@ p {
-
JumpServer {% trans 'Client' %} v1.1.6
+
JumpServer {% trans 'Client' %} v1.1.7
{% trans 'JumpServer Client, currently used to launch the client, now only support launch RDP SSH client, The Telnet client will next' %}
diff --git a/apps/terminal/api/endpoint.py b/apps/terminal/api/endpoint.py
index 225b3300a..f864c6d13 100644
--- a/apps/terminal/api/endpoint.py
+++ b/apps/terminal/api/endpoint.py
@@ -1,15 +1,16 @@
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status
+from rest_framework.request import Request
from common.drf.api import JMSBulkModelViewSet
from django.utils.translation import ugettext_lazy as _
from django.shortcuts import get_object_or_404
from assets.models import Asset
from orgs.utils import tmp_to_root_org
from terminal.models import Session
-from common.permissions import IsValidUser
from ..models import Endpoint, EndpointRule
from .. import serializers
+from common.permissions import IsValidUserOrConnectionToken
__all__ = ['EndpointViewSet', 'EndpointRuleViewSet']
@@ -17,39 +18,43 @@ __all__ = ['EndpointViewSet', 'EndpointRuleViewSet']
class SmartEndpointViewMixin:
get_serializer: callable
+ request: Request
- @action(methods=['get'], detail=False, permission_classes=[IsValidUser], url_path='smart')
+ # View 处理过程中用的属性
+ target_instance: None
+ target_protocol: None
+
+ @action(methods=['get'], detail=False, permission_classes=[IsValidUserOrConnectionToken],
+ url_path='smart')
def smart(self, request, *args, **kwargs):
- protocol = request.GET.get('protocol')
- if not protocol:
+ self.target_instance = self.get_target_instance()
+ self.target_protocol = self.get_target_protocol()
+ if not self.target_protocol:
error = _('Not found protocol query params')
return Response(data={'error': error}, status=status.HTTP_404_NOT_FOUND)
- endpoint = self.match_endpoint(request, protocol)
+ endpoint = self.match_endpoint()
serializer = self.get_serializer(endpoint)
return Response(serializer.data)
- def match_endpoint(self, request, protocol):
- instance = self.get_target_instance(request)
- endpoint = self.match_endpoint_by_label(instance, protocol)
+ def match_endpoint(self):
+ endpoint = self.match_endpoint_by_label()
if not endpoint:
- endpoint = self.match_endpoint_by_target_ip(request, instance, protocol)
+ endpoint = self.match_endpoint_by_target_ip()
return endpoint
- @staticmethod
- def match_endpoint_by_label(instance, protocol):
- return Endpoint.match_by_instance_label(instance, protocol)
+ def match_endpoint_by_label(self):
+ return Endpoint.match_by_instance_label(self.target_instance, self.target_protocol)
- @staticmethod
- def match_endpoint_by_target_ip(request, instance, protocol):
+ def match_endpoint_by_target_ip(self):
# 用来方便测试
- target_ip = request.GET.get('target_ip', '')
- if not target_ip and callable(getattr(instance, 'get_target_ip', None)):
- target_ip = instance.get_target_ip()
- endpoint = EndpointRule.match_endpoint(target_ip, protocol, request)
+ target_ip = self.request.GET.get('target_ip', '')
+ if not target_ip and callable(getattr(self.target_instance, 'get_target_ip', None)):
+ target_ip = self.target_instance.get_target_ip()
+ endpoint = EndpointRule.match_endpoint(target_ip, self.target_protocol, self.request)
return endpoint
- @staticmethod
- def get_target_instance(request):
+ def get_target_instance(self):
+ request = self.request
asset_id = request.GET.get('asset_id')
session_id = request.GET.get('session_id')
token_id = request.GET.get('token')
@@ -71,6 +76,14 @@ class SmartEndpointViewMixin:
instance = get_object_or_404(model, pk=pk)
return instance
+ def get_target_protocol(self):
+ protocol = None
+ if isinstance(self.target_instance, Application) and self.target_instance.is_type(Application.APP_TYPE.oracle):
+ protocol = self.target_instance.get_target_protocol_for_oracle()
+ if not protocol:
+ protocol = self.request.GET.get('protocol')
+ return protocol
+
class EndpointViewSet(SmartEndpointViewMixin, JMSBulkModelViewSet):
filterset_fields = ('name', 'host')
diff --git a/apps/terminal/backends/command/es.py b/apps/terminal/backends/command/es.py
index 1387f6656..20602769c 100644
--- a/apps/terminal/backends/command/es.py
+++ b/apps/terminal/backends/command/es.py
@@ -18,7 +18,7 @@ from common.utils.common import lazyproperty
from common.utils import get_logger
from common.utils.timezone import local_now_date_display, utc_now
from common.exceptions import JMSException
-from .models import AbstractSessionCommand
+from terminal.models import Command
logger = get_logger(__file__)
@@ -181,7 +181,7 @@ class CommandStore(object):
item['_source'].update({'id': item['_id']})
source_data.append(item['_source'])
- return AbstractSessionCommand.from_multi_dict(source_data)
+ return Command.from_multi_dict(source_data)
def count(self, **query):
body = self.get_query_body(**query)
diff --git a/apps/terminal/backends/command/models.py b/apps/terminal/backends/command/models.py
index 0a795b905..d6eb7a458 100644
--- a/apps/terminal/backends/command/models.py
+++ b/apps/terminal/backends/command/models.py
@@ -47,21 +47,6 @@ class AbstractSessionCommand(OrgModelMixin):
risk_mapper = dict(cls.RISK_LEVEL_CHOICES)
return risk_mapper.get(risk_level)
- @classmethod
- def from_dict(cls, d):
- self = cls()
- for k, v in d.items():
- setattr(self, k, v)
- return self
-
- @classmethod
- def from_multi_dict(cls, l):
- commands = []
- for d in l:
- command = cls.from_dict(d)
- commands.append(command)
- return commands
-
def to_dict(self):
d = {}
for field in self._meta.fields:
diff --git a/apps/terminal/migrations/0052_auto_20220713_1417.py b/apps/terminal/migrations/0052_auto_20220713_1417.py
new file mode 100644
index 000000000..87ad6ba6a
--- /dev/null
+++ b/apps/terminal/migrations/0052_auto_20220713_1417.py
@@ -0,0 +1,25 @@
+# Generated by Django 3.2.12 on 2022-07-13 06:17
+
+import common.db.fields
+import django.core.validators
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('terminal', '0051_sessionsharing_users'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='endpoint',
+ name='oracle_11g_port',
+ field=common.db.fields.PortField(default=15211, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='Oracle 11g Port'),
+ ),
+ migrations.AddField(
+ model_name='endpoint',
+ name='oracle_12c_port',
+ field=common.db.fields.PortField(default=15212, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='Oracle 12c Port'),
+ ),
+ ]
diff --git a/apps/terminal/models/command.py b/apps/terminal/models/command.py
index 3e94740ff..44edf013c 100644
--- a/apps/terminal/models/command.py
+++ b/apps/terminal/models/command.py
@@ -1,7 +1,5 @@
from __future__ import unicode_literals
-import time
-
from django.db import models
from django.db.models.signals import post_save
from django.utils.translation import ugettext_lazy as _
@@ -47,6 +45,21 @@ class Command(AbstractSessionCommand):
cls.objects.bulk_create(commands)
print(f'Create {len(commands)} commands of org ({org})')
+ @classmethod
+ def from_dict(cls, d):
+ self = cls()
+ for k, v in d.items():
+ setattr(self, k, v)
+ return self
+
+ @classmethod
+ def from_multi_dict(cls, l):
+ commands = []
+ for d in l:
+ command = cls.from_dict(d)
+ commands.append(command)
+ return commands
+
class Meta:
db_table = "terminal_command"
ordering = ('-timestamp',)
diff --git a/apps/terminal/models/endpoint.py b/apps/terminal/models/endpoint.py
index 36413f678..f823f6c90 100644
--- a/apps/terminal/models/endpoint.py
+++ b/apps/terminal/models/endpoint.py
@@ -18,6 +18,8 @@ class Endpoint(JMSBaseModel):
mariadb_port = PortField(default=33061, verbose_name=_('MariaDB Port'))
postgresql_port = PortField(default=54320, verbose_name=_('PostgreSQL Port'))
redis_port = PortField(default=63790, verbose_name=_('Redis Port'))
+ oracle_11g_port = PortField(default=15211, verbose_name=_('Oracle 11g Port'))
+ oracle_12c_port = PortField(default=15212, verbose_name=_('Oracle 12c Port'))
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
default_id = '00000000-0000-0000-0000-000000000001'
@@ -32,6 +34,10 @@ class Endpoint(JMSBaseModel):
def get_port(self, protocol):
return getattr(self, f'{protocol}_port', 0)
+ def get_oracle_port(self, version):
+ protocol = f'oracle_{version}'
+ return self.get_port(protocol)
+
def is_default(self):
return str(self.id) == self.default_id
@@ -57,8 +63,6 @@ class Endpoint(JMSBaseModel):
'http_port': 0,
}
endpoint, created = cls.objects.get_or_create(id=cls.default_id, defaults=data)
- if not endpoint.host and request:
- endpoint.host = request.get_host().split(':')[0]
return endpoint
@classmethod
@@ -116,4 +120,7 @@ class EndpointRule(JMSBaseModel):
endpoint = endpoint_rule.endpoint
else:
endpoint = Endpoint.get_or_create_default(request)
+ if not endpoint.host and request:
+ # 动态添加 current request host
+ endpoint.host = request.get_host().split(':')[0]
return endpoint
diff --git a/apps/terminal/models/sharing.py b/apps/terminal/models/sharing.py
index 6976b09cf..8675ced01 100644
--- a/apps/terminal/models/sharing.py
+++ b/apps/terminal/models/sharing.py
@@ -43,6 +43,8 @@ class SessionSharing(CommonModelMixin, OrgModelMixin):
return 'Creator: {}'.format(self.creator)
def users_display(self):
+ if not self.users:
+ return []
with tmp_to_root_org():
user_ids = self.users.split(',')
users = User.objects.filter(id__in=user_ids)
diff --git a/apps/terminal/notifications.py b/apps/terminal/notifications.py
index 62bc50592..91509a901 100644
--- a/apps/terminal/notifications.py
+++ b/apps/terminal/notifications.py
@@ -130,7 +130,7 @@ class CommandExecutionAlert(CommandAlertMixin, SystemMessage):
for asset in command['assets']:
url = reverse(
'assets:asset-detail', kwargs={'pk': asset.id},
- api_to_ui=True, external=True
+ api_to_ui=True, external=True, is_console=True
) + '?oid={}'.format(asset.org_id)
assets_with_url.append([asset, url])
diff --git a/apps/terminal/serializers/endpoint.py b/apps/terminal/serializers/endpoint.py
index fa3e71c35..3d8e858ac 100644
--- a/apps/terminal/serializers/endpoint.py
+++ b/apps/terminal/serializers/endpoint.py
@@ -8,6 +8,8 @@ __all__ = ['EndpointSerializer', 'EndpointRuleSerializer']
class EndpointSerializer(BulkModelSerializer):
+ # 解决 luna 处理繁琐的问题,oracle_port 返回匹配到的端口
+ oracle_port = serializers.SerializerMethodField(label=_('Oracle port'))
class Meta:
model = Endpoint
@@ -17,6 +19,8 @@ class EndpointSerializer(BulkModelSerializer):
'https_port', 'http_port', 'ssh_port',
'rdp_port', 'mysql_port', 'mariadb_port',
'postgresql_port', 'redis_port',
+ 'oracle_11g_port', 'oracle_12c_port',
+ 'oracle_port',
]
fields = fields_mini + fields_small + [
'comment', 'date_created', 'date_updated', 'created_by'
@@ -30,8 +34,16 @@ class EndpointSerializer(BulkModelSerializer):
'mariadb_port': {'default': 33061},
'postgresql_port': {'default': 54320},
'redis_port': {'default': 63790},
+ 'oracle_11g_port': {'default': 15211},
+ 'oracle_12c_port': {'default': 15212},
}
+ def get_oracle_port(self, obj: Endpoint):
+ view = self.context.get('view')
+ if not view or view.action not in ['smart']:
+ return 0
+ return obj.get_port(view.target_protocol)
+
class EndpointRuleSerializer(BulkModelSerializer):
_ip_group_help_text = '{}
{}'.format(
diff --git a/apps/terminal/serializers/session.py b/apps/terminal/serializers/session.py
index a22d17cdb..148217306 100644
--- a/apps/terminal/serializers/session.py
+++ b/apps/terminal/serializers/session.py
@@ -36,7 +36,7 @@ class SessionSerializer(BulkOrgResourceModelSerializer):
'is_success': {'label': _('Is success')},
'can_replay': {'label': _('Can replay')},
'can_join': {'label': _('Can join')},
- 'terminal': {'label': _('Terminal')},
+ 'terminal': {'label': _('Terminal ID')},
'is_finished': {'label': _('Is finished')},
'can_terminate': {'label': _('Can terminate')},
'terminal_display': {'label': _('Terminal display')},
diff --git a/apps/tickets/api/super_ticket.py b/apps/tickets/api/super_ticket.py
index ea186bd1b..32c4a56c0 100644
--- a/apps/tickets/api/super_ticket.py
+++ b/apps/tickets/api/super_ticket.py
@@ -20,4 +20,4 @@ class SuperTicketStatusAPI(RetrieveDestroyAPIView):
return Ticket.objects.all()
def perform_destroy(self, instance):
- instance.close(processor=instance.applicant)
+ instance.close()
diff --git a/apps/tickets/api/ticket.py b/apps/tickets/api/ticket.py
index 369bc155d..12b1cab1b 100644
--- a/apps/tickets/api/ticket.py
+++ b/apps/tickets/api/ticket.py
@@ -30,7 +30,8 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
serializer_class = serializers.TicketDisplaySerializer
serializer_classes = {
'list': serializers.TicketListSerializer,
- 'open': serializers.TicketApplySerializer
+ 'open': serializers.TicketApplySerializer,
+ 'approve': serializers.TicketApproveSerializer
}
model = Ticket
perm_model = Ticket
@@ -77,7 +78,8 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
partial = kwargs.pop('partial', False)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
- serializer.is_valid(raise_exception=True)
+ with tmp_to_root_org():
+ serializer.is_valid(raise_exception=True)
instance = serializer.save()
instance.approve(processor=request.user)
return Response('ok')
diff --git a/apps/tickets/handlers/apply_application.py b/apps/tickets/handlers/apply_application.py
index 15c76c7e5..287c70c4a 100644
--- a/apps/tickets/handlers/apply_application.py
+++ b/apps/tickets/handlers/apply_application.py
@@ -1,6 +1,6 @@
from django.utils.translation import ugettext as _
-from orgs.utils import tmp_to_org, tmp_to_root_org
+from orgs.utils import tmp_to_org
from perms.models import ApplicationPermission
from tickets.models import ApplyApplicationTicket
from .base import BaseHandler
@@ -16,16 +16,19 @@ class Handler(BaseHandler):
# permission
def _create_application_permission(self):
- with tmp_to_root_org():
+ org_id = self.ticket.org_id
+ with tmp_to_org(org_id):
application_permission = ApplicationPermission.objects.filter(id=self.ticket.id).first()
if application_permission:
return application_permission
+ apply_applications = self.ticket.apply_applications.all()
+ apply_system_users = self.ticket.apply_system_users.all()
+
apply_permission_name = self.ticket.apply_permission_name
+ apply_actions = self.ticket.apply_actions
apply_category = self.ticket.apply_category
apply_type = self.ticket.apply_type
- apply_applications = self.ticket.apply_applications.all()
- apply_system_users = self.ticket.apply_system_users.all()
apply_date_start = self.ticket.apply_date_start
apply_date_expired = self.ticket.apply_date_expired
permission_created_by = '{}:{}'.format(
@@ -48,6 +51,7 @@ class Handler(BaseHandler):
'name': apply_permission_name,
'from_ticket': True,
'category': apply_category,
+ 'actions': apply_actions,
'type': apply_type,
'comment': str(permission_comment),
'created_by': permission_created_by,
diff --git a/apps/tickets/handlers/apply_asset.py b/apps/tickets/handlers/apply_asset.py
index 136347782..e9d29697d 100644
--- a/apps/tickets/handlers/apply_asset.py
+++ b/apps/tickets/handlers/apply_asset.py
@@ -16,15 +16,17 @@ class Handler(BaseHandler):
# permission
def _create_asset_permission(self):
- with tmp_to_root_org():
+ org_id = self.ticket.org_id
+ with tmp_to_org(org_id):
asset_permission = AssetPermission.objects.filter(id=self.ticket.id).first()
if asset_permission:
return asset_permission
+ apply_nodes = self.ticket.apply_nodes.all()
+ apply_assets = self.ticket.apply_assets.all()
+ apply_system_users = self.ticket.apply_system_users.all()
+
apply_permission_name = self.ticket.apply_permission_name
- apply_nodes = self.ticket.apply_nodes.all()
- apply_assets = self.ticket.apply_assets.all()
- apply_system_users = self.ticket.apply_system_users.all()
apply_actions = self.ticket.apply_actions
apply_date_start = self.ticket.apply_date_start
apply_date_expired = self.ticket.apply_date_expired
diff --git a/apps/tickets/handlers/base.py b/apps/tickets/handlers/base.py
index c48ce350b..81341ce8e 100644
--- a/apps/tickets/handlers/base.py
+++ b/apps/tickets/handlers/base.py
@@ -64,9 +64,14 @@ class BaseHandler:
diff_context = {}
if state != TicketState.approved:
return diff_context
+
if self.ticket.type not in [TicketType.apply_asset, TicketType.apply_application]:
return diff_context
+ # 企业微信,钉钉审批不做diff
+ if not hasattr(self.ticket, 'old_rel_snapshot'):
+ return diff_context
+
old_rel_snapshot = self.ticket.old_rel_snapshot
current_rel_snapshot = self.ticket.get_local_snapshot()
diff = set(current_rel_snapshot.items()) - set(old_rel_snapshot.items())
diff --git a/apps/tickets/migrations/0016_auto_20220609_1758.py b/apps/tickets/migrations/0016_auto_20220609_1758.py
index ecf1c85fd..e5e6f61ce 100644
--- a/apps/tickets/migrations/0016_auto_20220609_1758.py
+++ b/apps/tickets/migrations/0016_auto_20220609_1758.py
@@ -139,7 +139,7 @@ class Migration(migrations.Migration):
('ticket_ptr',
models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True,
primary_key=True, serialize=False, to='tickets.ticket')),
- ('apply_permission_name', models.CharField(max_length=128, verbose_name='Apply name')),
+ ('apply_permission_name', models.CharField(max_length=128, verbose_name='Permission name')),
('apply_actions', models.IntegerField(
choices=[(255, 'All'), (1, 'Connect'), (2, 'Upload file'), (4, 'Download file'),
(6, 'Upload download'), (8, 'Clipboard copy'), (16, 'Clipboard paste'),
@@ -162,7 +162,7 @@ class Migration(migrations.Migration):
('ticket_ptr',
models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True,
primary_key=True, serialize=False, to='tickets.ticket')),
- ('apply_permission_name', models.CharField(max_length=128, verbose_name='Apply name')),
+ ('apply_permission_name', models.CharField(max_length=128, verbose_name='Permission name')),
('apply_category',
models.CharField(choices=[('db', 'Database'), ('remote_app', 'Remote app'), ('cloud', 'Cloud')],
max_length=16, verbose_name='Category')),
diff --git a/apps/tickets/migrations/0018_applyapplicationticket_apply_actions.py b/apps/tickets/migrations/0018_applyapplicationticket_apply_actions.py
new file mode 100644
index 000000000..74f5ed7a3
--- /dev/null
+++ b/apps/tickets/migrations/0018_applyapplicationticket_apply_actions.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1.14 on 2022-07-22 08:03
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('tickets', '0017_auto_20220623_1027'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='applyapplicationticket',
+ name='apply_actions',
+ field=models.IntegerField(choices=[(255, 'All'), (1, 'Connect'), (2, 'Upload file'), (4, 'Download file'), (6, 'Upload download'), (8, 'Clipboard copy'), (16, 'Clipboard paste'), (24, 'Clipboard copy paste')], default=255, verbose_name='Actions'),
+ ),
+ ]
diff --git a/apps/tickets/models/ticket/apply_application.py b/apps/tickets/models/ticket/apply_application.py
new file mode 100644
index 000000000..378047db2
--- /dev/null
+++ b/apps/tickets/models/ticket/apply_application.py
@@ -0,0 +1,45 @@
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+from perms.models import Action
+from applications.const import AppCategory, AppType
+from .general import Ticket
+
+__all__ = ['ApplyApplicationTicket']
+
+
+class ApplyApplicationTicket(Ticket):
+ apply_permission_name = models.CharField(max_length=128, verbose_name=_('Permission name'))
+ # 申请信息
+ apply_category = models.CharField(
+ max_length=16, choices=AppCategory.choices, verbose_name=_('Category')
+ )
+ apply_type = models.CharField(
+ max_length=16, choices=AppType.choices, verbose_name=_('Type')
+ )
+ apply_applications = models.ManyToManyField(
+ 'applications.Application', verbose_name=_('Apply applications'),
+ )
+ apply_system_users = models.ManyToManyField(
+ 'assets.SystemUser', verbose_name=_('Apply system users'),
+ )
+ apply_actions = models.IntegerField(
+ choices=Action.DB_CHOICES, default=Action.ALL, verbose_name=_('Actions')
+ )
+ apply_date_start = models.DateTimeField(verbose_name=_('Date start'), null=True)
+ apply_date_expired = models.DateTimeField(verbose_name=_('Date expired'), null=True)
+
+ @property
+ def apply_category_display(self):
+ return AppCategory.get_label(self.apply_category)
+
+ @property
+ def apply_type_display(self):
+ return AppType.get_label(self.apply_type)
+
+ @property
+ def apply_actions_display(self):
+ return Action.value_to_choices_display(self.apply_actions)
+
+ def get_apply_actions_display(self):
+ return ', '.join(self.apply_actions_display)
diff --git a/apps/tickets/notifications.py b/apps/tickets/notifications.py
index 3fa0e5a82..26997b0dc 100644
--- a/apps/tickets/notifications.py
+++ b/apps/tickets/notifications.py
@@ -87,7 +87,7 @@ class BaseTicketMessage(UserMessage):
@property
def spec_items(self):
fields = self.ticket._meta.local_fields + self.ticket._meta.local_many_to_many
- excludes = ['ticket_ptr']
+ excludes = ['ticket_ptr', 'flow']
item_names = [field.name for field in fields if field.name not in excludes]
return self._get_fields_items(item_names)
diff --git a/apps/tickets/serializers/flow.py b/apps/tickets/serializers/flow.py
index 42c7bed15..e8c066100 100644
--- a/apps/tickets/serializers/flow.py
+++ b/apps/tickets/serializers/flow.py
@@ -3,6 +3,7 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from orgs.models import Organization
+from orgs.utils import get_current_org_id
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
from tickets.models import TicketFlow, ApprovalRule
from tickets.const import TicketApprovalStrategy
@@ -96,7 +97,9 @@ class TicketFlowSerializer(OrgResourceModelSerializerMixin):
@atomic
def update(self, instance, validated_data):
- if instance.org_id == Organization.ROOT_ID:
+ current_org_id = get_current_org_id()
+ root_org_id = Organization.ROOT_ID
+ if instance.org_id == root_org_id and current_org_id != root_org_id:
instance = self.create(validated_data)
else:
instance = self.create_or_update('update', validated_data, instance)
diff --git a/apps/tickets/serializers/ticket/apply_application.py b/apps/tickets/serializers/ticket/apply_application.py
new file mode 100644
index 000000000..12b3f230d
--- /dev/null
+++ b/apps/tickets/serializers/ticket/apply_application.py
@@ -0,0 +1,65 @@
+from django.utils.translation import ugettext as _
+from rest_framework import serializers
+
+from perms.models import ApplicationPermission
+from perms.serializers.base import ActionsField
+from orgs.utils import tmp_to_org
+from applications.models import Application
+from tickets.models import ApplyApplicationTicket
+from .ticket import TicketApplySerializer
+from .common import BaseApplyAssetApplicationSerializer
+
+__all__ = ['ApplyApplicationSerializer', 'ApplyApplicationDisplaySerializer', 'ApproveApplicationSerializer']
+
+
+class ApplyApplicationSerializer(BaseApplyAssetApplicationSerializer, TicketApplySerializer):
+ apply_actions = ActionsField(required=True, allow_empty=False)
+ permission_model = ApplicationPermission
+
+ class Meta:
+ model = ApplyApplicationTicket
+ writeable_fields = [
+ 'id', 'title', 'type', 'apply_category',
+ 'apply_type', 'apply_applications', 'apply_system_users',
+ 'apply_actions', 'apply_date_start', 'apply_date_expired', 'org_id'
+ ]
+ fields = TicketApplySerializer.Meta.fields + \
+ writeable_fields + ['apply_permission_name', 'apply_actions_display']
+ read_only_fields = list(set(fields) - set(writeable_fields))
+ ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs
+ extra_kwargs = {
+ 'apply_applications': {'required': False, 'allow_empty': True},
+ 'apply_system_users': {'required': False, 'allow_empty': True},
+ }
+ extra_kwargs.update(ticket_extra_kwargs)
+
+ def validate_apply_applications(self, applications):
+ if self.is_final_approval and not applications:
+ raise serializers.ValidationError(_('This field is required.'))
+ tp = self.initial_data.get('apply_type')
+ return self.filter_many_to_many_field(Application, applications, type=tp)
+
+
+class ApproveApplicationSerializer(ApplyApplicationSerializer):
+ class Meta(ApplyApplicationSerializer.Meta):
+ read_only_fields = ApplyApplicationSerializer.Meta.read_only_fields + ['title', 'type']
+
+
+class ApplyApplicationDisplaySerializer(ApplyApplicationSerializer):
+ apply_applications = serializers.SerializerMethodField()
+ apply_system_users = serializers.SerializerMethodField()
+
+ class Meta:
+ model = ApplyApplicationSerializer.Meta.model
+ fields = ApplyApplicationSerializer.Meta.fields
+ read_only_fields = fields
+
+ @staticmethod
+ def get_apply_applications(instance):
+ with tmp_to_org(instance.org_id):
+ return instance.apply_applications.values_list('id', flat=True)
+
+ @staticmethod
+ def get_apply_system_users(instance):
+ with tmp_to_org(instance.org_id):
+ return instance.apply_system_users.values_list('id', flat=True)
diff --git a/apps/tickets/serializers/ticket/apply_asset.py b/apps/tickets/serializers/ticket/apply_asset.py
index b8a007e8d..93a4026c1 100644
--- a/apps/tickets/serializers/ticket/apply_asset.py
+++ b/apps/tickets/serializers/ticket/apply_asset.py
@@ -23,10 +23,11 @@ class ApplyAssetSerializer(BaseApplyAssetApplicationSerializer, TicketApplySeria
model = ApplyAssetTicket
writeable_fields = [
'id', 'title', 'type', 'apply_nodes', 'apply_assets',
- 'apply_system_users', 'apply_actions', 'apply_actions_display',
+ 'apply_system_users', 'apply_actions',
'apply_date_start', 'apply_date_expired', 'org_id'
]
- fields = TicketApplySerializer.Meta.fields + writeable_fields + ['apply_permission_name']
+ fields = TicketApplySerializer.Meta.fields + \
+ writeable_fields + ['apply_permission_name', 'apply_actions_display']
read_only_fields = list(set(fields) - set(writeable_fields))
ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs
extra_kwargs = {
diff --git a/apps/tickets/serializers/ticket/ticket.py b/apps/tickets/serializers/ticket/ticket.py
index facf3fcec..a0760e4d0 100644
--- a/apps/tickets/serializers/ticket/ticket.py
+++ b/apps/tickets/serializers/ticket/ticket.py
@@ -9,7 +9,7 @@ from tickets.models import Ticket, TicketFlow
from tickets.const import TicketType
__all__ = [
- 'TicketDisplaySerializer', 'TicketApplySerializer', 'TicketListSerializer'
+ 'TicketDisplaySerializer', 'TicketApplySerializer', 'TicketListSerializer', 'TicketApproveSerializer'
]
@@ -59,6 +59,13 @@ class TicketDisplaySerializer(TicketSerializer):
read_only_fields = fields
+class TicketApproveSerializer(TicketSerializer):
+ class Meta:
+ model = Ticket
+ fields = TicketSerializer.Meta.fields
+ read_only_fields = fields
+
+
class TicketApplySerializer(TicketSerializer):
org_id = serializers.CharField(
required=True, max_length=36, allow_blank=True, label=_("Organization")
diff --git a/apps/tickets/templates/tickets/ticket_approve_diff.html b/apps/tickets/templates/tickets/ticket_approve_diff.html
index 9fe6b80e0..8426b34ed 100644
--- a/apps/tickets/templates/tickets/ticket_approve_diff.html
+++ b/apps/tickets/templates/tickets/ticket_approve_diff.html
@@ -2,6 +2,7 @@
{{ approve_info }}
+{% if content %}
+{% endif %}
diff --git a/apps/users/api/profile.py b/apps/users/api/profile.py
index 3f5605144..b916c47a9 100644
--- a/apps/users/api/profile.py
+++ b/apps/users/api/profile.py
@@ -3,6 +3,10 @@ import uuid
from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
+from common.permissions import IsValidUserOrConnectionToken
+from common.utils import get_object_or_none
+from orgs.utils import tmp_to_root_org
+from authentication.models import ConnectionToken
from users.notifications import (
ResetPasswordMsg, ResetPasswordSuccessMsg, ResetSSHKeyMsg,
@@ -44,12 +48,26 @@ class UserResetPKApi(UserQuerysetMixin, generics.UpdateAPIView):
class UserProfileApi(generics.RetrieveUpdateAPIView):
- permission_classes = (IsAuthenticated,)
+ permission_classes = (IsValidUserOrConnectionToken,)
serializer_class = serializers.UserProfileSerializer
def get_object(self):
+ if self.request.user.is_anonymous:
+ user = self.get_connection_token_user()
+ if user:
+ return user
return self.request.user
+ def get_connection_token_user(self):
+ token_id = self.request.query_params.get('token')
+ if not token_id:
+ return
+ with tmp_to_root_org():
+ token = get_object_or_none(ConnectionToken, id=token_id)
+ if not token:
+ return
+ return token.user
+
class UserPasswordApi(generics.RetrieveUpdateAPIView):
permission_classes = (IsAuthenticated,)
diff --git a/apps/users/api/user.py b/apps/users/api/user.py
index a5d726fe0..70939f51f 100644
--- a/apps/users/api/user.py
+++ b/apps/users/api/user.py
@@ -33,7 +33,7 @@ __all__ = [
class UserViewSet(CommonApiMixin, UserQuerysetMixin, SuggestionMixin, BulkModelViewSet):
filterset_class = UserFilter
- search_fields = ('username', 'email', 'name', 'id', 'source', 'role', 'is_active')
+ search_fields = ('username', 'email', 'name')
serializer_classes = {
'default': UserSerializer,
'suggestion': MiniUserSerializer,
diff --git a/apps/users/models/user.py b/apps/users/models/user.py
index 2fd33aa08..78d5eb540 100644
--- a/apps/users/models/user.py
+++ b/apps/users/models/user.py
@@ -628,6 +628,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
radius = 'radius', 'Radius'
cas = 'cas', 'CAS'
saml2 = 'saml2', 'SAML2'
+ oauth2 = 'oauth2', 'OAuth2'
SOURCE_BACKEND_MAPPING = {
Source.local: [
@@ -652,6 +653,9 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
Source.saml2: [
settings.AUTH_BACKEND_SAML2
],
+ Source.oauth2: [
+ settings.AUTH_BACKEND_OAUTH2
+ ],
}
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
@@ -796,7 +800,8 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
def is_password_authenticate(self):
cas = self.Source.cas
saml2 = self.Source.saml2
- return self.source not in [cas, saml2]
+ oauth2 = self.Source.oauth2
+ return self.source not in [cas, saml2, oauth2]
def set_unprovide_attr_if_need(self):
if not self.name:
diff --git a/apps/users/signal_handlers.py b/apps/users/signal_handlers.py
index bd9b01845..bcf50173a 100644
--- a/apps/users/signal_handlers.py
+++ b/apps/users/signal_handlers.py
@@ -9,6 +9,7 @@ from django.db.models.signals import post_save
from authentication.backends.oidc.signals import openid_create_or_update_user
from authentication.backends.saml2.signals import saml2_create_or_update_user
+from authentication.backends.oauth2.signals import oauth2_create_or_update_user
from common.utils import get_logger
from common.decorator import on_transaction_commit
from .signals import post_user_create
@@ -26,16 +27,18 @@ def user_authenticated_handle(user, created, source, attrs=None, **kwargs):
user.source = source
user.save()
- if not created and settings.AUTH_SAML2_ALWAYS_UPDATE_USER:
+ if not attrs:
+ return
+
+ always_update = getattr(settings, 'AUTH_%s_ALWAYS_UPDATE_USER' % source.upper(), False)
+ if not created and always_update:
attr_whitelist = ('user', 'username', 'email', 'phone', 'comment')
logger.debug(
- "Receive saml2 user updated signal: {}, "
+ "Receive {} user updated signal: {}, "
"Update user info: {},"
"(Update only properties in the whitelist. [{}])"
- "".format(user, str(attrs), ','.join(attr_whitelist))
+ "".format(source, user, str(attrs), ','.join(attr_whitelist))
)
- if not attrs:
- return
for key, value in attrs.items():
if key in attr_whitelist and value:
setattr(user, key, value)
@@ -103,6 +106,12 @@ def on_saml2_create_or_update_user(sender, user, created, attrs, **kwargs):
user_authenticated_handle(user, created, source, attrs, **kwargs)
+@receiver(oauth2_create_or_update_user)
+def on_oauth2_create_or_update_user(sender, user, created, attrs, **kwargs):
+ source = user.Source.oauth2.value
+ user_authenticated_handle(user, created, source, attrs, **kwargs)
+
+
@receiver(populate_user)
def on_ldap_create_user(sender, user, ldap_user, **kwargs):
if user and user.username not in ['admin']:
diff --git a/apps/users/templates/users/forgot_password.html b/apps/users/templates/users/forgot_password.html
index 8289726f3..16c784250 100644
--- a/apps/users/templates/users/forgot_password.html
+++ b/apps/users/templates/users/forgot_password.html
@@ -6,6 +6,7 @@