diff --git a/jumpserver/docker-guacamole/Dockerfile b/jumpserver/docker-guacamole/Dockerfile index f78c485d73e161e184e5c73c16cc89b39d708820..85f0c0450b47de08daef453cf479220b94f7f27d 100644 --- a/jumpserver/docker-guacamole/Dockerfile +++ b/jumpserver/docker-guacamole/Dockerfile @@ -1,5 +1,7 @@ FROM library/tomcat:9-jre8 +COPY sources.list /etc/apt/sources.list + ENV ARCH=amd64 \ GUAC_VER=1.0.0 \ GUACAMOLE_HOME=/app/guacamole diff --git a/jumpserver/docker-guacamole/guacamole-auth-jumpserver-1.0.0.jar b/jumpserver/docker-guacamole/guacamole-auth-jumpserver-1.0.0.jar index 77c1817bc5342f2622e6da0f5710a018c0ec52c6..414c34a007375cbfbf5c34e34a43410ffd64099a 100644 Binary files a/jumpserver/docker-guacamole/guacamole-auth-jumpserver-1.0.0.jar and b/jumpserver/docker-guacamole/guacamole-auth-jumpserver-1.0.0.jar differ diff --git a/jumpserver/docker-guacamole/sources.list b/jumpserver/docker-guacamole/sources.list new file mode 100644 index 0000000000000000000000000000000000000000..20f9a2bbbc6f336d56955d65829500a2f9b6fe76 --- /dev/null +++ b/jumpserver/docker-guacamole/sources.list @@ -0,0 +1,8 @@ +deb http://mirrors.aliyun.com/debian/ stretch main non-free contrib +deb-src http://mirrors.aliyun.com/debian/ stretch main non-free contrib +deb http://mirrors.aliyun.com/debian-security stretch/updates main +deb-src http://mirrors.aliyun.com/debian-security stretch/updates main +deb http://mirrors.aliyun.com/debian/ stretch-updates main non-free contrib +deb-src http://mirrors.aliyun.com/debian/ stretch-updates main non-free contrib +deb http://mirrors.aliyun.com/debian/ stretch-backports main non-free contrib +deb-src http://mirrors.aliyun.com/debian/ stretch-backports main non-free contrib diff --git a/jumpserver/jumpserver/README.md b/jumpserver/jumpserver/README.md index 37a703847f127fe685513b870c918ce04c18f462..595677321e17d4c3e8f62613a41426b2f9a98c7d 100644 --- a/jumpserver/jumpserver/README.md +++ b/jumpserver/jumpserver/README.md @@ -17,157 +17,156 @@ Jumpserver 采纳分布式架构,支持多机房跨区域部署,支持横向 ## 核心功能列表 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
身份验证 Authentication登录认证 - 资源统一登录和认证 -
LDAP 认证 -
支持 OpenID,实现单点登录 -
多因子认证 - MFA(Google Authenticator) -
账号管理 Account集中账号管理 - 管理用户管理 -
系统用户管理 -
统一密码管理 - 资产密码托管 -
自动生成密码 -
密码自动推送 -
密码过期设置 -
批量密码变更(X-PACK) - 定期批量修改密码 -
生成随机密码 -
多云环境的资产纳管(X-PACK) - 对私有云、公有云资产统一纳管 -
授权控制 Authorization资产授权管理 - 资产树 -
资产或资产组灵活授权 -
节点内资产自动继承授权 -
RemoteApp(X-PACK) - 实现更细粒度的应用级授权 -
组织管理(X-PACK) - 实现多租户管理,权限隔离 -
多维度授权 - 可对用户、用户组或系统角色授权 -
指令限制 - 限制特权指令使用,支持黑白名单 -
统一文件传输 - SFTP 文件上传/下载 -
文件管理 - Web SFTP 文件管理 -
安全审计 Audit会话管理 - 在线会话管理 -
历史会话管理 -
录像管理 - Linux 录像支持 -
Windows 录像支持 -
指令审计 - 指令记录 -
文件传输审计 - 上传/下载记录审计 -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
身份认证
Authentication
登录认证资源统一登录与认证
LDAP/AD 认证
RADIUS 认证
OpenID 认证(实现单点登录)
MFA认证MFA 二次认证(Google Authenticator)
RADIUS 二次认证
登录复核(X-PACK)用户登录行为受管理员的监管与控制
账号管理
Account
集中账号管理用户管理
系统用户管理
统一密码资产密码托管
自动生成密码
自动推送密码
密码过期设置
批量改密(X-PACK)定期批量改密
多种密码策略
多云纳管(X-PACK)对私有云、公有云资产自动统一纳管
收集用户(X-PACK)自定义任务定期收集主机用户
密码匣子(X-PACK)统一对资产主机的用户密码进行查看、更新、测试操作
授权控制
Authorization
多维授权对用户、用户组、资产、资产节点、应用以及系统用户进行授权
资产授权资产以树状结构进行展示
资产和节点均可灵活授权
节点内资产自动继承授权
子节点自动继承父节点授权
应用授权实现更细粒度的应用级授权
MySQL 数据库应用、RemoteApp 远程应用(X-PACK)
动作授权实现对授权资产的文件上传、下载以及连接动作的控制
时间授权实现对授权资源使用时间段的限制
特权指令实现对特权指令的使用(支持黑白名单)
命令过滤实现对授权系统用户所执行的命令进行控制
文件传输SFTP 文件上传/下载
文件管理实现 Web SFTP 文件管理
工单管理(X-PACK)支持对用户登录请求行为进行控制
组织管理(X-PACK)实现多租户管理与权限隔离
安全审计
Audit
操作审计用户操作行为审计
会话审计在线会话内容审计
历史会话内容审计
录像审计支持对 Linux、Windows 等资产操作的录像进行回放审计
支持对 RemoteApp(X-PACK)、MySQL 等应用操作的录像进行回放审计
指令审计支持对资产和应用等操作的命令进行审计
文件传输可对文件的上传、下载记录进行审计
## 安装及使用指南 diff --git a/jumpserver/jumpserver/apps/applications/api/__init__.py b/jumpserver/jumpserver/apps/applications/api/__init__.py index e6bc7adb428d7ac5d90f754ea02a927a5fa959e8..a707cfde694740144de5bf622242c76d469946d4 100644 --- a/jumpserver/jumpserver/apps/applications/api/__init__.py +++ b/jumpserver/jumpserver/apps/applications/api/__init__.py @@ -1 +1,2 @@ from .remote_app import * +from .database_app import * diff --git a/jumpserver/jumpserver/apps/applications/api/database_app.py b/jumpserver/jumpserver/apps/applications/api/database_app.py new file mode 100644 index 0000000000000000000000000000000000000000..af5810e0f0f53f097929fc25086304f5ace599b9 --- /dev/null +++ b/jumpserver/jumpserver/apps/applications/api/database_app.py @@ -0,0 +1,20 @@ +# coding: utf-8 +# + +from orgs.mixins.api import OrgBulkModelViewSet + +from .. import models +from .. import serializers +from ..hands import IsOrgAdminOrAppUser + +__all__ = [ + 'DatabaseAppViewSet', +] + + +class DatabaseAppViewSet(OrgBulkModelViewSet): + model = models.DatabaseApp + filter_fields = ('name',) + search_fields = filter_fields + permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = serializers.DatabaseAppSerializer diff --git a/jumpserver/jumpserver/apps/applications/const.py b/jumpserver/jumpserver/apps/applications/const.py index a5b6da8953ee049ddd072a92d53493735fbb5160..af3531c3618aea2db75b7f3733d8399fc8ac89d0 100644 --- a/jumpserver/jumpserver/apps/applications/const.py +++ b/jumpserver/jumpserver/apps/applications/const.py @@ -5,6 +5,7 @@ from django.utils.translation import ugettext_lazy as _ # RemoteApp + REMOTE_APP_BOOT_PROGRAM_NAME = '||jmservisor' REMOTE_APP_TYPE_CHROME = 'chrome' @@ -12,29 +13,6 @@ REMOTE_APP_TYPE_MYSQL_WORKBENCH = 'mysql_workbench' REMOTE_APP_TYPE_VMWARE_CLIENT = 'vmware_client' REMOTE_APP_TYPE_CUSTOM = 'custom' -REMOTE_APP_TYPE_CHOICES = ( - ( - _('Browser'), - ( - (REMOTE_APP_TYPE_CHROME, 'Chrome'), - ) - ), - ( - _('Database tools'), - ( - (REMOTE_APP_TYPE_MYSQL_WORKBENCH, 'MySQL Workbench'), - ) - ), - ( - _('Virtualization tools'), - ( - (REMOTE_APP_TYPE_VMWARE_CLIENT, 'vSphere Client'), - ) - ), - (REMOTE_APP_TYPE_CUSTOM, _('Custom')), - -) - # Fields attribute write_only default => False REMOTE_APP_TYPE_CHROME_FIELDS = [ @@ -60,9 +38,26 @@ REMOTE_APP_TYPE_CUSTOM_FIELDS = [ {'name': 'custom_password', 'write_only': True} ] -REMOTE_APP_TYPE_MAP_FIELDS = { +REMOTE_APP_TYPE_FIELDS_MAP = { REMOTE_APP_TYPE_CHROME: REMOTE_APP_TYPE_CHROME_FIELDS, REMOTE_APP_TYPE_MYSQL_WORKBENCH: REMOTE_APP_TYPE_MYSQL_WORKBENCH_FIELDS, REMOTE_APP_TYPE_VMWARE_CLIENT: REMOTE_APP_TYPE_VMWARE_CLIENT_FIELDS, REMOTE_APP_TYPE_CUSTOM: REMOTE_APP_TYPE_CUSTOM_FIELDS } + +REMOTE_APP_TYPE_CHOICES = ( + (REMOTE_APP_TYPE_CHROME, 'Chrome'), + (REMOTE_APP_TYPE_MYSQL_WORKBENCH, 'MySQL Workbench'), + (REMOTE_APP_TYPE_VMWARE_CLIENT, 'vSphere Client'), + (REMOTE_APP_TYPE_CUSTOM, _('Custom')), +) + + +# DatabaseApp + + +DATABASE_APP_TYPE_MYSQL = 'mysql' + +DATABASE_APP_TYPE_CHOICES = ( + (DATABASE_APP_TYPE_MYSQL, 'MySQL'), +) diff --git a/jumpserver/jumpserver/apps/applications/forms/__init__.py b/jumpserver/jumpserver/apps/applications/forms/__init__.py index e6bc7adb428d7ac5d90f754ea02a927a5fa959e8..a707cfde694740144de5bf622242c76d469946d4 100644 --- a/jumpserver/jumpserver/apps/applications/forms/__init__.py +++ b/jumpserver/jumpserver/apps/applications/forms/__init__.py @@ -1 +1,2 @@ from .remote_app import * +from .database_app import * diff --git a/jumpserver/jumpserver/apps/applications/forms/database_app.py b/jumpserver/jumpserver/apps/applications/forms/database_app.py new file mode 100644 index 0000000000000000000000000000000000000000..2b5a4c0cf96fb7921627dcc65dca41b44f06bf8b --- /dev/null +++ b/jumpserver/jumpserver/apps/applications/forms/database_app.py @@ -0,0 +1,26 @@ +# coding: utf-8 +# + + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from .. import models + +__all__ = ['DatabaseAppMySQLForm'] + + +class BaseDatabaseAppForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['type'].widget.attrs['disabled'] = True + + class Meta: + model = models.DatabaseApp + fields = [ + 'name', 'type', 'host', 'port', 'database', 'comment' + ] + + +class DatabaseAppMySQLForm(BaseDatabaseAppForm): + pass diff --git a/jumpserver/jumpserver/apps/applications/forms/remote_app.py b/jumpserver/jumpserver/apps/applications/forms/remote_app.py index b12759462e87e40afb3e5b9bbe67480c6a993c25..7a097fcc8b3589ee4cc9f757b6b9e41f7cec6fb7 100644 --- a/jumpserver/jumpserver/apps/applications/forms/remote_app.py +++ b/jumpserver/jumpserver/apps/applications/forms/remote_app.py @@ -5,18 +5,52 @@ from django.utils.translation import ugettext as _ from django import forms from orgs.mixins.forms import OrgModelForm -from assets.models import SystemUser from ..models import RemoteApp -from .. import const __all__ = [ - 'RemoteAppCreateUpdateForm', + 'RemoteAppChromeForm', 'RemoteAppMySQLWorkbenchForm', + 'RemoteAppVMwareForm', 'RemoteAppCustomForm' ] -class RemoteAppTypeChromeForm(forms.ModelForm): +class BaseRemoteAppForm(OrgModelForm): + default_initial_data = {} + + def __init__(self, *args, **kwargs): + # 过滤RDP资产和系统用户 + super().__init__(*args, **kwargs) + field_asset = self.fields['asset'] + field_asset.queryset = field_asset.queryset.has_protocol('rdp') + self.fields['type'].widget.attrs['disabled'] = True + self.fields.move_to_end('comment') + self.initial_default() + + def initial_default(self): + for name, value in self.default_initial_data.items(): + field = self.fields.get(name) + if not field: + continue + field.initial = value + + class Meta: + model = RemoteApp + fields = [ + 'name', 'asset', 'type', 'path', 'comment' + ] + widgets = { + 'asset': forms.Select(attrs={ + 'class': 'select2', 'data-placeholder': _('Asset') + }), + } + + +class RemoteAppChromeForm(BaseRemoteAppForm): + default_initial_data = { + 'path': r'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe' + } + chrome_target = forms.CharField( max_length=128, label=_('Target URL'), required=False ) @@ -29,7 +63,12 @@ class RemoteAppTypeChromeForm(forms.ModelForm): ) -class RemoteAppTypeMySQLWorkbenchForm(forms.ModelForm): +class RemoteAppMySQLWorkbenchForm(BaseRemoteAppForm): + default_initial_data = { + 'path': r'C:\Program Files\MySQL\MySQL Workbench 8.0 CE' + r'\MySQLWorkbench.exe' + } + mysql_workbench_ip = forms.CharField( max_length=128, label=_('Database IP'), required=False ) @@ -45,7 +84,12 @@ class RemoteAppTypeMySQLWorkbenchForm(forms.ModelForm): ) -class RemoteAppTypeVMwareForm(forms.ModelForm): +class RemoteAppVMwareForm(BaseRemoteAppForm): + default_initial_data = { + 'path': r'C:\Program Files (x86)\VMware\Infrastructure' + r'\Virtual Infrastructure Client\Launcher\VpxClient.exe' + } + vmware_target = forms.CharField( max_length=128, label=_('Target address'), required=False ) @@ -58,7 +102,8 @@ class RemoteAppTypeVMwareForm(forms.ModelForm): ) -class RemoteAppTypeCustomForm(forms.ModelForm): +class RemoteAppCustomForm(BaseRemoteAppForm): + custom_cmdline = forms.CharField( max_length=128, label=_('Operating parameter'), required=False ) @@ -73,51 +118,3 @@ class RemoteAppTypeCustomForm(forms.ModelForm): max_length=128, label=_('Login password'), required=False ) - -class RemoteAppTypeForms( - RemoteAppTypeChromeForm, - RemoteAppTypeMySQLWorkbenchForm, - RemoteAppTypeVMwareForm, - RemoteAppTypeCustomForm -): - pass - - -class RemoteAppCreateUpdateForm(RemoteAppTypeForms, OrgModelForm): - def __init__(self, *args, **kwargs): - # 过滤RDP资产和系统用户 - super().__init__(*args, **kwargs) - field_asset = self.fields['asset'] - field_asset.queryset = field_asset.queryset.has_protocol('rdp') - - class Meta: - model = RemoteApp - fields = [ - 'name', 'asset', 'type', 'path', 'comment' - ] - widgets = { - 'asset': forms.Select(attrs={ - 'class': 'select2', 'data-placeholder': _('Asset') - }), - } - - def _clean_params(self): - app_type = self.data.get('type') - fields = const.REMOTE_APP_TYPE_MAP_FIELDS.get(app_type, []) - params = {} - for field in fields: - name = field['name'] - value = self.cleaned_data[name] - params.update({name: value}) - return params - - def _save_params(self, instance): - params = self._clean_params() - instance.params = params - instance.save() - return instance - - def save(self, commit=True): - instance = super().save(commit=commit) - instance = self._save_params(instance) - return instance diff --git a/jumpserver/jumpserver/apps/applications/migrations/0003_auto_20191210_1659.py b/jumpserver/jumpserver/apps/applications/migrations/0003_auto_20191210_1659.py new file mode 100644 index 0000000000000000000000000000000000000000..fc3e4cdf501ef658753907c44db4b858d1c9b821 --- /dev/null +++ b/jumpserver/jumpserver/apps/applications/migrations/0003_auto_20191210_1659.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.11 on 2019-12-10 08:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('applications', '0002_remove_remoteapp_system_user'), + ] + + operations = [ + migrations.AlterField( + model_name='remoteapp', + name='type', + field=models.CharField(choices=[('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom')], default='chrome', max_length=128, verbose_name='App type'), + ), + ] diff --git a/jumpserver/jumpserver/apps/applications/migrations/0004_auto_20191218_1705.py b/jumpserver/jumpserver/apps/applications/migrations/0004_auto_20191218_1705.py new file mode 100644 index 0000000000000000000000000000000000000000..f22d2e29091258cb7534c83c9af61dcf3eec73c3 --- /dev/null +++ b/jumpserver/jumpserver/apps/applications/migrations/0004_auto_20191218_1705.py @@ -0,0 +1,38 @@ +# Generated by Django 2.1.11 on 2019-12-18 09:05 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('applications', '0003_auto_20191210_1659'), + ] + + operations = [ + migrations.CreateModel( + name='DatabaseApp', + fields=[ + ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('type', models.CharField(choices=[('mysql', 'MySQL')], default='mysql', max_length=128, verbose_name='Type')), + ('host', models.CharField(db_index=True, max_length=128, verbose_name='Host')), + ('port', models.IntegerField(default=3306, verbose_name='Port')), + ('database', models.CharField(blank=True, db_index=True, max_length=128, null=True, verbose_name='Database')), + ('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')), + ], + options={ + 'verbose_name': 'DatabaseApp', + 'ordering': ('name',), + }, + ), + migrations.AlterUniqueTogether( + name='databaseapp', + unique_together={('org_id', 'name')}, + ), + ] diff --git a/jumpserver/jumpserver/apps/applications/models/__init__.py b/jumpserver/jumpserver/apps/applications/models/__init__.py index e6bc7adb428d7ac5d90f754ea02a927a5fa959e8..a707cfde694740144de5bf622242c76d469946d4 100644 --- a/jumpserver/jumpserver/apps/applications/models/__init__.py +++ b/jumpserver/jumpserver/apps/applications/models/__init__.py @@ -1 +1,2 @@ from .remote_app import * +from .database_app import * diff --git a/jumpserver/jumpserver/apps/applications/models/database_app.py b/jumpserver/jumpserver/apps/applications/models/database_app.py new file mode 100644 index 0000000000000000000000000000000000000000..3317f06a4f6b76c0b5c1bdf50bdc8bffdaf140bd --- /dev/null +++ b/jumpserver/jumpserver/apps/applications/models/database_app.py @@ -0,0 +1,42 @@ +# coding: utf-8 +# + +import uuid +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from orgs.mixins.models import OrgModelMixin +from common.mixins import CommonModelMixin +from .. import const + + +__all__ = ['DatabaseApp'] + + +class DatabaseApp(CommonModelMixin, OrgModelMixin): + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + name = models.CharField(max_length=128, verbose_name=_('Name')) + type = models.CharField( + default=const.DATABASE_APP_TYPE_MYSQL, + choices=const.DATABASE_APP_TYPE_CHOICES, + max_length=128, verbose_name=_('Type') + ) + host = models.CharField( + max_length=128, verbose_name=_('Host'), db_index=True + ) + port = models.IntegerField(default=3306, verbose_name=_('Port')) + database = models.CharField( + max_length=128, blank=True, null=True, verbose_name=_('Database'), + db_index=True + ) + comment = models.TextField( + max_length=128, default='', blank=True, verbose_name=_('Comment') + ) + + def __str__(self): + return self.name + + class Meta: + unique_together = [('org_id', 'name'), ] + verbose_name = _("DatabaseApp") + ordering = ('name', ) diff --git a/jumpserver/jumpserver/apps/applications/models/remote_app.py b/jumpserver/jumpserver/apps/applications/models/remote_app.py index 17746833c6e704dac052f4cf17ce53bd02e0acb9..b9aee0ade7d2e5fc4cf4404cfc1a89f36c16a9a3 100644 --- a/jumpserver/jumpserver/apps/applications/models/remote_app.py +++ b/jumpserver/jumpserver/apps/applications/models/remote_app.py @@ -62,7 +62,7 @@ class RemoteApp(OrgModelMixin): _parameters.append(self.type) path = '\"%s\"' % self.path _parameters.append(path) - for field in const.REMOTE_APP_TYPE_MAP_FIELDS[self.type]: + for field in const.REMOTE_APP_TYPE_FIELDS_MAP[self.type]: value = self.params.get(field['name']) if value is None: continue diff --git a/jumpserver/jumpserver/apps/applications/serializers/__init__.py b/jumpserver/jumpserver/apps/applications/serializers/__init__.py index e6bc7adb428d7ac5d90f754ea02a927a5fa959e8..a707cfde694740144de5bf622242c76d469946d4 100644 --- a/jumpserver/jumpserver/apps/applications/serializers/__init__.py +++ b/jumpserver/jumpserver/apps/applications/serializers/__init__.py @@ -1 +1,2 @@ from .remote_app import * +from .database_app import * diff --git a/jumpserver/jumpserver/apps/applications/serializers/database_app.py b/jumpserver/jumpserver/apps/applications/serializers/database_app.py new file mode 100644 index 0000000000000000000000000000000000000000..43b87319370ffc246fffffd05e67cc8099d19016 --- /dev/null +++ b/jumpserver/jumpserver/apps/applications/serializers/database_app.py @@ -0,0 +1,26 @@ +# coding: utf-8 +# + +from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from common.serializers import AdaptedBulkListSerializer + +from .. import models + +__all__ = [ + 'DatabaseAppSerializer', +] + + +class DatabaseAppSerializer(BulkOrgResourceModelSerializer): + + class Meta: + model = models.DatabaseApp + list_serializer_class = AdaptedBulkListSerializer + fields = [ + 'id', 'name', 'type', 'get_type_display', 'host', 'port', + 'database', 'comment', 'created_by', 'date_created', 'date_updated', + ] + read_only_fields = [ + 'created_by', 'date_created', 'date_updated' + 'get_type_display', + ] diff --git a/jumpserver/jumpserver/apps/applications/serializers/remote_app.py b/jumpserver/jumpserver/apps/applications/serializers/remote_app.py index 168a0228021009f631025d83e9c2f21d214af343..fb9a7a270b8f5323286e3b725a9b29d2c5dc0862 100644 --- a/jumpserver/jumpserver/apps/applications/serializers/remote_app.py +++ b/jumpserver/jumpserver/apps/applications/serializers/remote_app.py @@ -1,10 +1,11 @@ # coding: utf-8 # - +import copy from rest_framework import serializers from common.serializers import AdaptedBulkListSerializer +from common.fields.serializer import CustomMetaDictField from orgs.mixins.serializers import BulkOrgResourceModelSerializer from .. import const @@ -16,72 +17,54 @@ __all__ = [ ] -class RemoteAppParamsDictField(serializers.DictField): - """ - RemoteApp field => params - """ - @staticmethod - def filter_attribute(attribute, instance): - """ - 过滤掉params字段值中write_only特性的key-value值 - For example, the chrome_password field is not returned when serializing - { - 'chrome_target': 'http://www.jumpserver.org/', - 'chrome_username': 'admin', - 'chrome_password': 'admin', - } - """ - for field in const.REMOTE_APP_TYPE_MAP_FIELDS[instance.type]: - if field.get('write_only', False): - attribute.pop(field['name'], None) - return attribute - - def get_attribute(self, instance): - """ - 序列化时调用 - """ - attribute = super().get_attribute(instance) - attribute = self.filter_attribute(attribute, instance) - return attribute - - @staticmethod - def filter_value(dictionary, value): - """ - 过滤掉不属于当前app_type所包含的key-value值 - """ - app_type = dictionary.get('type', const.REMOTE_APP_TYPE_CHROME) - fields = const.REMOTE_APP_TYPE_MAP_FIELDS[app_type] - fields_names = [field['name'] for field in fields] - no_need_keys = [k for k in value.keys() if k not in fields_names] - for k in no_need_keys: - value.pop(k) - return value - - def get_value(self, dictionary): - """ - 反序列化时调用 - """ - value = super().get_value(dictionary) - value = self.filter_value(dictionary, value) - return value +class RemoteAppParamsDictField(CustomMetaDictField): + type_fields_map = const.REMOTE_APP_TYPE_FIELDS_MAP + default_type = const.REMOTE_APP_TYPE_CHROME + convert_key_remove_type_prefix = False + convert_key_to_upper = False class RemoteAppSerializer(BulkOrgResourceModelSerializer): params = RemoteAppParamsDictField() + type_fields_map = const.REMOTE_APP_TYPE_FIELDS_MAP class Meta: model = RemoteApp list_serializer_class = AdaptedBulkListSerializer fields = [ - 'id', 'name', 'asset', 'type', 'path', 'params', - 'comment', 'created_by', 'date_created', 'asset_info', - 'get_type_display', + 'id', 'name', 'asset', 'asset_info', 'type', 'get_type_display', + 'path', 'params', 'date_created', 'created_by', 'comment', ] read_only_fields = [ 'created_by', 'date_created', 'asset_info', 'get_type_display' ] + def process_params(self, instance, validated_data): + new_params = copy.deepcopy(validated_data.get('params', {})) + tp = validated_data.get('type', '') + + if tp != instance.type: + return new_params + + old_params = instance.params + fields = self.type_fields_map.get(instance.type, []) + for field in fields: + if not field.get('write_only', False): + continue + field_name = field['name'] + new_value = new_params.get(field_name, '') + old_value = old_params.get(field_name, '') + field_value = new_value if new_value else old_value + new_params[field_name] = field_value + + return new_params + + def update(self, instance, validated_data): + params = self.process_params(instance, validated_data) + validated_data['params'] = params + return super().update(instance, validated_data) + class RemoteAppConnectionInfoSerializer(serializers.ModelSerializer): parameter_remote_app = serializers.SerializerMethodField() diff --git a/jumpserver/jumpserver/apps/applications/templates/applications/database_app_create_update.html b/jumpserver/jumpserver/apps/applications/templates/applications/database_app_create_update.html new file mode 100644 index 0000000000000000000000000000000000000000..84635e2d0c7871b1210e0797af545932ec9aead6 --- /dev/null +++ b/jumpserver/jumpserver/apps/applications/templates/applications/database_app_create_update.html @@ -0,0 +1,55 @@ +{% extends '_base_create_update.html' %} +{% load static %} +{% load bootstrap3 %} +{% load i18n %} + +{% block form %} +
+ {% bootstrap_form form layout="horizontal" %} +
+
+
+ + +
+
+
+{% endblock %} + +{% block custom_foot_js %} + +{% endblock %} \ No newline at end of file diff --git a/jumpserver/jumpserver/apps/applications/templates/applications/database_app_detail.html b/jumpserver/jumpserver/apps/applications/templates/applications/database_app_detail.html new file mode 100644 index 0000000000000000000000000000000000000000..153bfa98ae09770954ffb6f17d3122c3eaa1b7d6 --- /dev/null +++ b/jumpserver/jumpserver/apps/applications/templates/applications/database_app_detail.html @@ -0,0 +1,103 @@ +{% extends 'base.html' %} +{% load static %} +{% load i18n %} + +{% block content %} +
+
+
+
+ +
+
+
+
+ {{ database_app.name }} +
+ + + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{% trans 'Name' %}:{{ database_app.name }}
{% trans 'Type' %}:{{ database_app.get_type_display }}
{% trans 'Host' %}:{{ database_app.host }}
{% trans 'Port' %}:{{ database_app.port }}
{% trans 'Database' %}:{{ database_app.database }}
{% trans 'Date created' %}:{{ database_app.date_created }}
{% trans 'Created by' %}:{{ database_app.created_by }}
{% trans 'Comment' %}:{{ database_app.comment }}
+
+
+
+
+
+
+
+
+{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/jumpserver/jumpserver/apps/applications/templates/applications/database_app_list.html b/jumpserver/jumpserver/apps/applications/templates/applications/database_app_list.html new file mode 100644 index 0000000000000000000000000000000000000000..74a5c907ecae955ad7363db00b19fb817cdff5c4 --- /dev/null +++ b/jumpserver/jumpserver/apps/applications/templates/applications/database_app_list.html @@ -0,0 +1,88 @@ +{% extends '_base_list.html' %} +{% load i18n static %} +{% block help_message %} +{% endblock %} +{% block table_search %}{% endblock %} +{% block table_container %} +
+ + +
+ + + + + + + + + + + + + + + +
+ + {% trans 'Name' %}{% trans 'Type' %}{% trans 'Host' %}{% trans 'Port' %}{% trans 'Database' %}{% trans 'Comment' %}{% trans 'Action' %}
+{% endblock %} +{% block content_bottom_left %}{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/jumpserver/jumpserver/apps/applications/templates/applications/remote_app_create_update.html b/jumpserver/jumpserver/apps/applications/templates/applications/remote_app_create_update.html index b193dfff5ef899e54634f233321a534d85eb7b7a..440219936cc79f188123b76c81fc975bfdefd2ad 100644 --- a/jumpserver/jumpserver/apps/applications/templates/applications/remote_app_create_update.html +++ b/jumpserver/jumpserver/apps/applications/templates/applications/remote_app_create_update.html @@ -4,51 +4,8 @@ {% load i18n %} {% block form %} -
- {% if form.non_field_errors %} -
- {{ form.non_field_errors }} -
- {% endif %} - {% csrf_token %} - {% bootstrap_field form.name layout="horizontal" %} - {% bootstrap_field form.asset layout="horizontal" %} - {% bootstrap_field form.type layout="horizontal" %} - {% bootstrap_field form.path layout="horizontal" %} - -
- - {# chrome #} -
- {% bootstrap_field form.chrome_target layout="horizontal" %} - {% bootstrap_field form.chrome_username layout="horizontal" %} - {% bootstrap_field form.chrome_password layout="horizontal" %} -
- - {# mysql workbench #} -
- {% bootstrap_field form.mysql_workbench_ip layout="horizontal" %} - {% bootstrap_field form.mysql_workbench_name layout="horizontal" %} - {% bootstrap_field form.mysql_workbench_username layout="horizontal" %} - {% bootstrap_field form.mysql_workbench_password layout="horizontal" %} -
- - {# vmware #} -
- {% bootstrap_field form.vmware_target layout="horizontal" %} - {% bootstrap_field form.vmware_username layout="horizontal" %} - {% bootstrap_field form.vmware_password layout="horizontal" %} -
- - {# custom #} -
- {% bootstrap_field form.custom_cmdline layout="horizontal" %} - {% bootstrap_field form.custom_target layout="horizontal" %} - {% bootstrap_field form.custom_username layout="horizontal" %} - {% bootstrap_field form.custom_password layout="horizontal" %} -
- - {% bootstrap_field form.comment layout="horizontal" %} + + {% bootstrap_form form layout="horizontal" %}
@@ -57,93 +14,49 @@
-
{% endblock %} {% block custom_foot_js %} -{% endblock %} - {% block content %}
@@ -102,4 +97,4 @@ $(document).ready(function () { objectDelete($this, name, the_url, redirect_url); }) -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/jumpserver/jumpserver/apps/applications/templates/applications/remote_app_list.html b/jumpserver/jumpserver/apps/applications/templates/applications/remote_app_list.html index 3dbe4f8ebfb893e6cceb54ff4399310580d5e793..709152489697855c205b624f0d6bc37d052c0fe3 100644 --- a/jumpserver/jumpserver/apps/applications/templates/applications/remote_app_list.html +++ b/jumpserver/jumpserver/apps/applications/templates/applications/remote_app_list.html @@ -1,15 +1,20 @@ {% extends '_base_list.html' %} {% load i18n static %} {% block help_message %} -
{% trans 'Before using this feature, make sure that the application loader has been uploaded to the application server and successfully published as a RemoteApp application' %} {% trans 'Download application loader' %} -
{% endblock %} {% block table_search %}{% endblock %} {% block table_container %} -
- {% trans "Create RemoteApp" %} +
+ +
@@ -63,7 +68,7 @@ function initTable() { {data: "get_type_display", orderable: false}, {data: "asset_info", orderable: false}, {data: "comment"}, - {data: "id", orderable: false} + {data: "id", orderable: false, width: "120px"} ], op_html: $('#actions').html() }; @@ -84,4 +89,4 @@ $(document).ready(function(){ }, 3000); }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/jumpserver/jumpserver/apps/applications/templates/applications/user_database_app_list.html b/jumpserver/jumpserver/apps/applications/templates/applications/user_database_app_list.html new file mode 100644 index 0000000000000000000000000000000000000000..1edaacd76f5c8523b78e728c32fad10c0c7821b5 --- /dev/null +++ b/jumpserver/jumpserver/apps/applications/templates/applications/user_database_app_list.html @@ -0,0 +1,83 @@ +{% extends 'base.html' %} +{% load i18n static %} + +{% block custom_head_css_js %} + +{% endblock %} + +{% block content %} +
+
+ + + + + + + + + + + + + +
+ + {% trans 'Name' %}{% trans 'Type' %}{% trans 'Host' %}{% trans 'Database' %}{% trans 'Comment' %}{% trans 'Action' %}
+
+{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/jumpserver/jumpserver/apps/applications/urls/api_urls.py b/jumpserver/jumpserver/apps/applications/urls/api_urls.py index 6384f5dacdf6e54c2d1267a2bc51a21a629f0f9e..1186bf1a23b83ccc2dde71b0e1ac7ea48c7ebb4e 100644 --- a/jumpserver/jumpserver/apps/applications/urls/api_urls.py +++ b/jumpserver/jumpserver/apps/applications/urls/api_urls.py @@ -11,10 +11,12 @@ app_name = 'applications' router = BulkRouter() router.register(r'remote-apps', api.RemoteAppViewSet, 'remote-app') +router.register(r'database-apps', api.DatabaseAppViewSet, 'database-app') urlpatterns = [ path('remote-apps//connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'), ] + old_version_urlpatterns = [ re_path('(?Premote-app)/.*', capi.redirect_plural_name_api) ] diff --git a/jumpserver/jumpserver/apps/applications/urls/views_urls.py b/jumpserver/jumpserver/apps/applications/urls/views_urls.py index 3ffcffc5c754e3ffac6a92af4f3814283a071c64..663b0878da17bb820e2dc84f3ba7ce55e7692e6e 100644 --- a/jumpserver/jumpserver/apps/applications/urls/views_urls.py +++ b/jumpserver/jumpserver/apps/applications/urls/views_urls.py @@ -11,6 +11,13 @@ urlpatterns = [ path('remote-app//update/', views.RemoteAppUpdateView.as_view(), name='remote-app-update'), path('remote-app//', views.RemoteAppDetailView.as_view(), name='remote-app-detail'), # User RemoteApp view - path('user-remote-app/', views.UserRemoteAppListView.as_view(), name='user-remote-app-list') + path('user-remote-app/', views.UserRemoteAppListView.as_view(), name='user-remote-app-list'), + + path('database-app/', views.DatabaseAppListView.as_view(), name='database-app-list'), + path('database-app/create/', views.DatabaseAppCreateView.as_view(), name='database-app-create'), + path('database-app//update/', views.DatabaseAppUpdateView.as_view(), name='database-app-update'), + path('database-app//', views.DatabaseAppDetailView.as_view(), name='database-app-detail'), + # User DatabaseApp view + path('user-database-app/', views.UserDatabaseAppListView.as_view(), name='user-database-app-list'), ] diff --git a/jumpserver/jumpserver/apps/applications/views/__init__.py b/jumpserver/jumpserver/apps/applications/views/__init__.py index e6bc7adb428d7ac5d90f754ea02a927a5fa959e8..a707cfde694740144de5bf622242c76d469946d4 100644 --- a/jumpserver/jumpserver/apps/applications/views/__init__.py +++ b/jumpserver/jumpserver/apps/applications/views/__init__.py @@ -1 +1,2 @@ from .remote_app import * +from .database_app import * diff --git a/jumpserver/jumpserver/apps/applications/views/database_app.py b/jumpserver/jumpserver/apps/applications/views/database_app.py new file mode 100644 index 0000000000000000000000000000000000000000..21d2b4f7cd59e14a66e6207c9630dc2804ab5349 --- /dev/null +++ b/jumpserver/jumpserver/apps/applications/views/database_app.py @@ -0,0 +1,115 @@ +# coding: utf-8 +# + +from django.http import Http404 +from django.views.generic import TemplateView +from django.views.generic.edit import CreateView, UpdateView +from django.utils.translation import ugettext_lazy as _ +from django.views.generic.detail import DetailView + +from common.permissions import PermissionsMixin, IsOrgAdmin, IsValidUser + +from .. import models, const, forms + +__all__ = [ + 'DatabaseAppListView', 'DatabaseAppCreateView', 'DatabaseAppUpdateView', + 'DatabaseAppDetailView', 'UserDatabaseAppListView', +] + + +class DatabaseAppListView(PermissionsMixin, TemplateView): + template_name = 'applications/database_app_list.html' + permission_classes = [IsOrgAdmin] + + def get_context_data(self, **kwargs): + context = { + 'app': _("Application"), + 'action': _('DatabaseApp list'), + 'type_choices': const.DATABASE_APP_TYPE_CHOICES + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class BaseDatabaseAppCreateUpdateView: + template_name = 'applications/database_app_create_update.html' + model = models.DatabaseApp + permission_classes = [IsOrgAdmin] + default_type = const.DATABASE_APP_TYPE_MYSQL + form_class = forms.DatabaseAppMySQLForm + form_class_choices = { + const.DATABASE_APP_TYPE_MYSQL: forms.DatabaseAppMySQLForm, + } + + def get_initial(self): + return {'type': self.get_type()} + + def get_type(self): + return self.default_type + + def get_form_class(self): + tp = self.get_type() + form_class = self.form_class_choices.get(tp) + if not form_class: + raise Http404() + return form_class + + +class DatabaseAppCreateView(BaseDatabaseAppCreateUpdateView, CreateView): + + def get_type(self): + tp = self.request.GET.get("type") + if tp: + return tp.lower() + return super().get_type() + + def get_context_data(self, **kwargs): + context = { + 'app': _('Applications'), + 'action': _('Create DatabaseApp'), + 'api_action': 'create' + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class DatabaseAppUpdateView(BaseDatabaseAppCreateUpdateView, UpdateView): + + def get_type(self): + return self.object.type + + def get_context_data(self, **kwargs): + context = { + 'app': _('Applications'), + 'action': _('Create DatabaseApp'), + 'api_action': 'update' + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class DatabaseAppDetailView(PermissionsMixin, DetailView): + template_name = 'applications/database_app_detail.html' + model = models.DatabaseApp + context_object_name = 'database_app' + permission_classes = [IsOrgAdmin] + + def get_context_data(self, **kwargs): + context = { + 'app': _('Applications'), + 'action': _('DatabaseApp detail'), + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class UserDatabaseAppListView(PermissionsMixin, TemplateView): + template_name = 'applications/user_database_app_list.html' + permission_classes = [IsValidUser] + + def get_context_data(self, **kwargs): + context = { + 'action': _('My DatabaseApp'), + } + kwargs.update(context) + return super().get_context_data(**kwargs) diff --git a/jumpserver/jumpserver/apps/applications/views/remote_app.py b/jumpserver/jumpserver/apps/applications/views/remote_app.py index e7f6f0ccd06c9c903dfc1c37fe3a6be813803709..92b4360057a75ee2346b79425c27e89ba0987bab 100644 --- a/jumpserver/jumpserver/apps/applications/views/remote_app.py +++ b/jumpserver/jumpserver/apps/applications/views/remote_app.py @@ -1,19 +1,16 @@ # coding: utf-8 # +from django.http import Http404 from django.utils.translation import ugettext as _ from django.views.generic import TemplateView from django.views.generic.edit import CreateView, UpdateView from django.views.generic.detail import DetailView -from django.contrib.messages.views import SuccessMessageMixin -from django.urls import reverse_lazy - from common.permissions import PermissionsMixin, IsOrgAdmin, IsValidUser -from common.const import create_success_msg, update_success_msg from ..models import RemoteApp -from .. import forms +from .. import forms, const __all__ = [ @@ -30,53 +27,79 @@ class RemoteAppListView(PermissionsMixin, TemplateView): context = { 'app': _('Applications'), 'action': _('RemoteApp list'), + 'type_choices': const.REMOTE_APP_TYPE_CHOICES, } kwargs.update(context) return super().get_context_data(**kwargs) -class RemoteAppCreateView(PermissionsMixin, SuccessMessageMixin, CreateView): +class BaseRemoteAppCreateUpdateView: template_name = 'applications/remote_app_create_update.html' model = RemoteApp - form_class = forms.RemoteAppCreateUpdateForm - success_url = reverse_lazy('applications:remote-app-list') permission_classes = [IsOrgAdmin] + default_type = const.REMOTE_APP_TYPE_CHROME + form_class = forms.RemoteAppChromeForm + form_class_choices = { + const.REMOTE_APP_TYPE_CHROME: forms.RemoteAppChromeForm, + const.REMOTE_APP_TYPE_MYSQL_WORKBENCH: forms.RemoteAppMySQLWorkbenchForm, + const.REMOTE_APP_TYPE_VMWARE_CLIENT: forms.RemoteAppVMwareForm, + const.REMOTE_APP_TYPE_CUSTOM: forms.RemoteAppCustomForm + } + + def get_initial(self): + return {'type': self.get_type()} + + def get_type(self): + return self.default_type + + def get_form_class(self): + tp = self.get_type() + form_class = self.form_class_choices.get(tp) + if not form_class: + raise Http404() + return form_class + + +class RemoteAppCreateView(BaseRemoteAppCreateUpdateView, + PermissionsMixin, CreateView): def get_context_data(self, **kwargs): context = { 'app': _('Applications'), 'action': _('Create RemoteApp'), - 'type': 'create' + 'api_action': 'create' } kwargs.update(context) return super().get_context_data(**kwargs) - def get_success_message(self, cleaned_data): - return create_success_msg % ({'name': cleaned_data['name']}) + def get_type(self): + tp = self.request.GET.get("type") + if tp: + return tp.lower() + return super().get_type() -class RemoteAppUpdateView(PermissionsMixin, SuccessMessageMixin, UpdateView): - template_name = 'applications/remote_app_create_update.html' - model = RemoteApp - form_class = forms.RemoteAppCreateUpdateForm - success_url = reverse_lazy('applications:remote-app-list') - permission_classes = [IsOrgAdmin] +class RemoteAppUpdateView(BaseRemoteAppCreateUpdateView, + PermissionsMixin, UpdateView): def get_initial(self): - return {k: v for k, v in self.object.params.items()} + initial_data = super().get_initial() + params = {k: v for k, v in self.object.params.items()} + initial_data.update(params) + return initial_data + + def get_type(self): + return self.object.type def get_context_data(self, **kwargs): context = { 'app': _('Applications'), 'action': _('Update RemoteApp'), - 'type': 'update' + 'api_action': 'update' } kwargs.update(context) return super().get_context_data(**kwargs) - def get_success_message(self, cleaned_data): - return update_success_msg % ({'name': cleaned_data['name']}) - class RemoteAppDetailView(PermissionsMixin, DetailView): template_name = 'applications/remote_app_detail.html' diff --git a/jumpserver/jumpserver/apps/assets/api/__init__.py b/jumpserver/jumpserver/apps/assets/api/__init__.py index e7126878da08ab7afa42bc8ec2c0d73317255425..342f71bd4a8d86a89583993cf3684c3fcef4e258 100644 --- a/jumpserver/jumpserver/apps/assets/api/__init__.py +++ b/jumpserver/jumpserver/apps/assets/api/__init__.py @@ -2,6 +2,7 @@ from .admin_user import * from .asset import * from .label import * from .system_user import * +from .system_user_relation import * from .node import * from .domain import * from .cmd_filter import * diff --git a/jumpserver/jumpserver/apps/assets/api/admin_user.py b/jumpserver/jumpserver/apps/assets/api/admin_user.py index b91f10c6582b4e49d3d02a81c3c0e8f9b84fb36c..c77da1ae402e5be126f586883b32ebdf15a2a712 100644 --- a/jumpserver/jumpserver/apps/assets/api/admin_user.py +++ b/jumpserver/jumpserver/apps/assets/api/admin_user.py @@ -14,7 +14,10 @@ # limitations under the License. from django.db import transaction +from django.db.models import Count from django.shortcuts import get_object_or_404 +from django.utils.translation import ugettext as _ +from rest_framework import status from rest_framework.response import Response from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins import generics @@ -44,6 +47,19 @@ class AdminUserViewSet(OrgBulkModelViewSet): serializer_class = serializers.AdminUserSerializer permission_classes = (IsOrgAdmin,) + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.annotate(_assets_amount=Count('assets')) + return queryset + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + has_related_asset = instance.assets.exists() + if has_related_asset: + data = {'msg': _('Deleted failed, There are related assets')} + return Response(data=data, status=status.HTTP_400_BAD_REQUEST) + return super().destroy(request, *args, **kwargs) + class AdminUserAuthApi(generics.UpdateAPIView): model = AdminUser diff --git a/jumpserver/jumpserver/apps/assets/api/asset.py b/jumpserver/jumpserver/apps/assets/api/asset.py index 64fdc16dccebfb02c5cf74cd74fc358fbec297ae..0b63522ea0a48ea6d9332273255fc8a8740e5c07 100644 --- a/jumpserver/jumpserver/apps/assets/api/asset.py +++ b/jumpserver/jumpserver/apps/assets/api/asset.py @@ -4,24 +4,27 @@ import random from rest_framework.response import Response +from rest_framework.viewsets import ModelViewSet +from rest_framework.generics import RetrieveAPIView from django.shortcuts import get_object_or_404 from common.utils import get_logger, get_object_or_none -from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser +from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsSuperUser from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins import generics -from ..models import Asset, Node +from ..models import Asset, Node, Platform from .. import serializers -from ..tasks import update_asset_hardware_info_manual, \ - test_asset_connectivity_manual +from ..tasks import ( + update_asset_hardware_info_manual, test_asset_connectivity_manual +) from ..filters import AssetByNodeFilterBackend, LabelFilterBackend logger = get_logger(__file__) __all__ = [ - 'AssetViewSet', + 'AssetViewSet', 'AssetPlatformRetrieveApi', 'AssetRefreshHardwareApi', 'AssetAdminUserTestApi', - 'AssetGatewayApi', + 'AssetGatewayApi', 'AssetPlatformViewSet', ] @@ -53,6 +56,34 @@ class AssetViewSet(OrgBulkModelViewSet): self.set_assets_node(assets) +class AssetPlatformRetrieveApi(RetrieveAPIView): + queryset = Platform.objects.all() + permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = serializers.PlatformSerializer + + 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): + queryset = Platform.objects.all() + permission_classes = (IsSuperUser,) + 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 AssetRefreshHardwareApi(generics.RetrieveAPIView): """ Refresh asset hardware info diff --git a/jumpserver/jumpserver/apps/assets/api/asset_user.py b/jumpserver/jumpserver/apps/assets/api/asset_user.py index a07544e57ecc9f05efb0741dda3decaea9d71318..a3b9d89068a7eb41c821ea504b941f6f5ad5d709 100644 --- a/jumpserver/jumpserver/apps/assets/api/asset_user.py +++ b/jumpserver/jumpserver/apps/assets/api/asset_user.py @@ -114,7 +114,7 @@ class AssetUserExportViewSet(AssetUserViewSet): permission_classes = [IsOrgAdminOrAppUser] def get_permissions(self): - if settings.CONFIG.SECURITY_VIEW_AUTH_NEED_MFA: + if settings.SECURITY_VIEW_AUTH_NEED_MFA: self.permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify] return super().get_permissions() @@ -124,7 +124,7 @@ class AssetUserAuthInfoApi(generics.RetrieveAPIView): permission_classes = [IsOrgAdminOrAppUser] def get_permissions(self): - if settings.CONFIG.SECURITY_VIEW_AUTH_NEED_MFA: + if settings.SECURITY_VIEW_AUTH_NEED_MFA: self.permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify] return super().get_permissions() diff --git a/jumpserver/jumpserver/apps/assets/api/domain.py b/jumpserver/jumpserver/apps/assets/api/domain.py index b12f8ce2c70b58e890e56b0079e454bc6f3b0ab2..0e7108fd4c6ac1f6abbd73ff4bf0d405a0d3ce67 100644 --- a/jumpserver/jumpserver/apps/assets/api/domain.py +++ b/jumpserver/jumpserver/apps/assets/api/domain.py @@ -29,8 +29,8 @@ class DomainViewSet(OrgBulkModelViewSet): class GatewayViewSet(OrgBulkModelViewSet): model = Gateway - filter_fields = ("domain__name", "name", "username", "ip", "domain__id") - search_fields = filter_fields + filter_fields = ("domain__name", "name", "username", "ip", "domain") + search_fields = ("domain__name", "name", "username", "ip") permission_classes = (IsOrgAdmin,) serializer_class = serializers.GatewaySerializer diff --git a/jumpserver/jumpserver/apps/assets/api/node.py b/jumpserver/jumpserver/apps/assets/api/node.py index 1451650d2fd3af09e28b55a24cf6defda929f6da..24661bad6638d5cbfc432dabaaa2689d959603bc 100644 --- a/jumpserver/jumpserver/apps/assets/api/node.py +++ b/jumpserver/jumpserver/apps/assets/api/node.py @@ -177,7 +177,7 @@ class NodeChildrenAsTreeApi(NodeChildrenApi): if not include_assets: return queryset assets = self.instance.get_assets().only( - "id", "hostname", "ip", 'platform', "os", + "id", "hostname", "ip", "os", "org_id", "protocols", ) for asset in assets: diff --git a/jumpserver/jumpserver/apps/assets/api/system_user.py b/jumpserver/jumpserver/apps/assets/api/system_user.py index f1213f0a3f9b38ba74aaaa378cf77a3ed0e59e1b..e3dddd6f59eb62afe2e7d477e073d03d852aaa50 100644 --- a/jumpserver/jumpserver/apps/assets/api/system_user.py +++ b/jumpserver/jumpserver/apps/assets/api/system_user.py @@ -14,8 +14,8 @@ # limitations under the License. from django.shortcuts import get_object_or_404 -from django.conf import settings from rest_framework.response import Response +from django.db.models import Count from common.serializers import CeleryTaskSerializer from common.utils import get_logger @@ -50,6 +50,11 @@ class SystemUserViewSet(OrgBulkModelViewSet): serializer_class = serializers.SystemUserSerializer permission_classes = (IsOrgAdminOrAppUser,) + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.annotate(_assets_amount=Count('assets')) + return queryset + class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView): """ diff --git a/jumpserver/jumpserver/apps/assets/api/system_user_relation.py b/jumpserver/jumpserver/apps/assets/api/system_user_relation.py new file mode 100644 index 0000000000000000000000000000000000000000..de88cfe388fd5c088f8b36a3b9aae57d3e29f3a6 --- /dev/null +++ b/jumpserver/jumpserver/apps/assets/api/system_user_relation.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# +from django.db.models import F, Value +from django.db.models.functions import Concat + +from common.permissions import IsOrgAdmin +from orgs.mixins.api import OrgBulkModelViewSet +from orgs.utils import current_org +from .. import models, serializers + +__all__ = ['SystemUserAssetRelationViewSet', 'SystemUserNodeRelationViewSet'] + + +class RelationMixin(OrgBulkModelViewSet): + def get_queryset(self): + queryset = self.model.objects.all() + org_id = current_org.org_id() + if org_id is not None: + queryset = queryset.filter(systemuser__org_id=org_id) + queryset = queryset.annotate(systemuser_display=Concat( + F('systemuser__name'), Value('('), F('systemuser__username'), + Value(')') + )) + return queryset + + +class SystemUserAssetRelationViewSet(RelationMixin): + serializer_class = serializers.SystemUserAssetRelationSerializer + model = models.SystemUser.assets.through + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', 'asset', 'systemuser', + ] + search_fields = [ + "id", "asset__hostname", "asset__ip", + "systemuser__name", "systemuser__username" + ] + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.annotate( + asset_display=Concat( + F('asset__hostname'), Value('('), + F('asset__ip'), Value(')') + ) + ) + return queryset + + +class SystemUserNodeRelationViewSet(RelationMixin): + serializer_class = serializers.SystemUserNodeRelationSerializer + model = models.SystemUser.nodes.through + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', 'node', 'systemuser', + ] + search_fields = [ + "node__value", "systemuser__name", "systemuser_username" + ] + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset \ + .annotate(node_key=F('node__key')) + return queryset diff --git a/jumpserver/jumpserver/apps/assets/backends/base.py b/jumpserver/jumpserver/apps/assets/backends/base.py index d46c67f700249a85178a814a8308e7561508ed9d..9c9f7ca1e381f9284049595f47794e364b8f7c20 100644 --- a/jumpserver/jumpserver/apps/assets/backends/base.py +++ b/jumpserver/jumpserver/apps/assets/backends/base.py @@ -43,7 +43,7 @@ class AssetUserQuerySet(list): else: in_kwargs[k] = v for k in in_kwargs: - kwargs.pop(k) + kwargs.pop(k, None) if len(in_kwargs) == 0: return self @@ -56,7 +56,7 @@ class AssetUserQuerySet(list): v = [str(i) for i in v] if isinstance(attr, uuid.UUID): attr = str(attr) - if v in attr: + if attr in v: matched = True if matched: queryset.append(i) @@ -68,11 +68,12 @@ class AssetUserQuerySet(list): real = [] for k, v in kwargs.items(): wanted.append(v) - value = getattr(obj, k) + value = getattr(obj, k, None) if isinstance(value, uuid.UUID): value = str(value) real.append(value) return wanted == real + kwargs = {k: v for k, v in kwargs.items() if k.find('__in') == -1} if len(kwargs) > 0: queryset = AssetUserQuerySet([i for i in self if filter_it(i)]) else: diff --git a/jumpserver/jumpserver/apps/assets/const.py b/jumpserver/jumpserver/apps/assets/const.py index 7786d8f13e9301b4b296d5695b7188eea4cfd1da..ec51c5a2b9dd623073fa752065a5b5a615780c7f 100644 --- a/jumpserver/jumpserver/apps/assets/const.py +++ b/jumpserver/jumpserver/apps/assets/const.py @@ -1,14 +1,2 @@ # -*- coding: utf-8 -*- # - -from django.utils.translation import ugettext_lazy as _ - - -GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_HELP_TEXT = _( - 'Cannot contain special characters: [ {} ]' -).format(" ".join(['/', '\\'])) - -GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_PATTERN = r"[/\\]" - -GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_ERROR_MSG = \ - _("* The contains characters that are not allowed") diff --git a/jumpserver/jumpserver/apps/assets/forms/__init__.py b/jumpserver/jumpserver/apps/assets/forms/__init__.py index a086cb12cf87725e9e033f10c38045e7ad6c251c..39b39a45a942f085aefef91d3a23e7fd0467d609 100644 --- a/jumpserver/jumpserver/apps/assets/forms/__init__.py +++ b/jumpserver/jumpserver/apps/assets/forms/__init__.py @@ -5,3 +5,4 @@ from .label import * from .user import * from .domain import * from .cmd_filter import * +from .platform import * diff --git a/jumpserver/jumpserver/apps/assets/forms/asset.py b/jumpserver/jumpserver/apps/assets/forms/asset.py index 805f49f244e56a69e09a667c550a4838163a5a40..3a7a0f2207a84c05737fabb0712df94f1940d5bc 100644 --- a/jumpserver/jumpserver/apps/assets/forms/asset.py +++ b/jumpserver/jumpserver/apps/assets/forms/asset.py @@ -6,13 +6,12 @@ from django.utils.translation import gettext_lazy as _ from common.utils import get_logger from orgs.mixins.forms import OrgModelForm -from ..models import Asset, Node -from ..const import GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_HELP_TEXT +from ..models import Asset logger = get_logger(__file__) __all__ = [ - 'AssetCreateForm', 'AssetUpdateForm', 'AssetBulkUpdateForm', 'ProtocolForm', + 'AssetCreateUpdateForm', 'AssetBulkUpdateForm', 'ProtocolForm', ] @@ -27,17 +26,27 @@ class ProtocolForm(forms.Form): ) -class AssetCreateForm(OrgModelForm): +class AssetCreateUpdateForm(OrgModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - if self.data: - return + self.set_platform_to_name() + self.set_fields_queryset() + + def set_fields_queryset(self): nodes_field = self.fields['nodes'] + nodes_choices = [] if self.instance: - nodes_field.choices = [(n.id, n.full_value) for n in - self.instance.nodes.all()] - else: - nodes_field.choices = [] + nodes_choices = [ + (n.id, n.full_value) for n in + self.instance.nodes.all() + ] + nodes_field.choices = nodes_choices + + def set_platform_to_name(self): + platform_field = self.fields['platform'] + platform_field.to_field_name = 'name' + if self.instance: + self.initial['platform'] = self.instance.platform.name def add_nodes_initial(self, node): nodes_field = self.fields['nodes'] @@ -49,7 +58,7 @@ class AssetCreateForm(OrgModelForm): fields = [ 'hostname', 'ip', 'public_ip', 'protocols', 'comment', 'nodes', 'is_active', 'admin_user', 'labels', 'platform', - 'domain', + 'domain', 'number', ] widgets = { 'nodes': forms.SelectMultiple(attrs={ @@ -64,59 +73,14 @@ class AssetCreateForm(OrgModelForm): 'domain': forms.Select(attrs={ 'class': 'select2', 'data-placeholder': _('Domain') }), - } - labels = { - 'nodes': _("Node"), - } - help_texts = { - 'hostname': GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_HELP_TEXT, - 'admin_user': _( - 'root or other NOPASSWD sudo privilege user existed in asset,' - 'If asset is windows or other set any one, more see admin user left menu' - ), - 'platform': _("Windows 2016 RDP protocol is different, If is window 2016, set it"), - 'domain': _("If your have some network not connect with each other, you can set domain") - } - - -class AssetUpdateForm(OrgModelForm): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if self.data: - return - nodes_field = self.fields['nodes'] - if self.instance: - nodes_field.choices = ((n.id, n.full_value) for n in - self.instance.nodes.all()) - else: - nodes_field.choices = [] - - class Meta: - model = Asset - fields = [ - 'hostname', 'ip', 'protocols', 'nodes', 'is_active', 'platform', - 'public_ip', 'number', 'comment', 'admin_user', 'labels', - 'domain', - ] - widgets = { - 'nodes': forms.SelectMultiple(attrs={ - 'class': 'nodes-select2', 'data-placeholder': _('Node') - }), - 'admin_user': forms.Select(attrs={ - 'class': 'select2', 'data-placeholder': _('Admin user') - }), - 'labels': forms.SelectMultiple(attrs={ - 'class': 'select2', 'data-placeholder': _('Label') - }), - 'domain': forms.Select(attrs={ - 'class': 'select2', 'data-placeholder': _('Domain') + 'platform': forms.Select(attrs={ + 'class': 'select2', 'data-placeholder': _('Platform') }), } labels = { 'nodes': _("Node"), } help_texts = { - 'hostname': GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_HELP_TEXT, 'admin_user': _( 'root or other NOPASSWD sudo privilege user existed in asset,' 'If asset is windows or other set any one, more see admin user left menu' diff --git a/jumpserver/jumpserver/apps/assets/forms/platform.py b/jumpserver/jumpserver/apps/assets/forms/platform.py new file mode 100644 index 0000000000000000000000000000000000000000..88c4365d4867f4ce1dca519dc311edab16ba5d74 --- /dev/null +++ b/jumpserver/jumpserver/apps/assets/forms/platform.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from ..models import Platform + + +__all__ = ['PlatformForm', 'PlatformMetaForm'] + + +class PlatformMetaForm(forms.Form): + SECURITY_CHOICES = ( + ('rdp', "RDP"), + ('nla', "NLA"), + ('tls', 'TLS'), + ('any', "Any"), + ) + CONSOLE_CHOICES = ( + (True, _('Yes')), + (False, _('No')), + ) + security = forms.ChoiceField( + choices=SECURITY_CHOICES, initial='any', label=_("RDP security"), + required=False, + ) + console = forms.ChoiceField( + choices=CONSOLE_CHOICES, initial=False, label=_("RDP console"), + required=False, + ) + + +class PlatformForm(forms.ModelForm): + class Meta: + model = Platform + fields = [ + 'name', 'base', 'comment', + ] + labels = { + 'base': _("Base platform") + } + diff --git a/jumpserver/jumpserver/apps/assets/forms/user.py b/jumpserver/jumpserver/apps/assets/forms/user.py index 21626f4f6413b6d90d8800444f7ff755c9ee8022..3d67b434b16af5b0ac1fdc65bb24a317eed53f16 100644 --- a/jumpserver/jumpserver/apps/assets/forms/user.py +++ b/jumpserver/jumpserver/apps/assets/forms/user.py @@ -6,7 +6,6 @@ from django.utils.translation import gettext_lazy as _ from common.utils import validate_ssh_private_key, ssh_pubkey_gen, get_logger from orgs.mixins.forms import OrgModelForm from ..models import AdminUser, SystemUser -from ..const import GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_HELP_TEXT logger = get_logger(__file__) __all__ = [ @@ -99,7 +98,6 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm): }), } help_texts = { - 'name': GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_HELP_TEXT, 'auto_push': _('Auto push system user to asset'), 'priority': _('1-100, High level will be using login asset as default, ' 'if user was granted more than 2 system user'), diff --git a/jumpserver/jumpserver/apps/assets/migrations/0043_auto_20191114_1111.py b/jumpserver/jumpserver/apps/assets/migrations/0043_auto_20191114_1111.py new file mode 100644 index 0000000000000000000000000000000000000000..a07dee6bb0690ad0c27ec3c12c218b67b2583ca5 --- /dev/null +++ b/jumpserver/jumpserver/apps/assets/migrations/0043_auto_20191114_1111.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.5 on 2019-11-14 03:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0042_favoriteasset'), + ] + + operations = [ + migrations.AddField( + model_name='gathereduser', + name='date_last_login', + field=models.DateTimeField(null=True, verbose_name='Date last login'), + ), + migrations.AddField( + model_name='gathereduser', + name='ip_last_login', + field=models.CharField(default='', max_length=39, verbose_name='IP last login'), + ), + ] diff --git a/jumpserver/jumpserver/apps/assets/migrations/0044_platform.py b/jumpserver/jumpserver/apps/assets/migrations/0044_platform.py new file mode 100644 index 0000000000000000000000000000000000000000..8d45a8ee3ca276fde7b7d9d20d22ab93cb6f31c8 --- /dev/null +++ b/jumpserver/jumpserver/apps/assets/migrations/0044_platform.py @@ -0,0 +1,48 @@ +# Generated by Django 2.2.7 on 2019-12-06 07:26 + +import common.fields.model +from django.db import migrations, models + + +def create_internal_platform(apps, schema_editor): + model = apps.get_model("assets", "Platform") + db_alias = schema_editor.connection.alias + type_platforms = ( + ('Linux', 'Linux', None), + ('Unix', 'Unix', None), + ('MacOS', 'MacOS', None), + ('BSD', 'BSD', None), + ('Windows', 'Windows', None), + ('Windows2016', 'Windows', {'security': 'tls'}), + ('Other', 'Other', None), + ) + for name, base, meta in type_platforms: + model.objects.using(db_alias).create( + name=name, base=base, internal=True, meta=meta + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0043_auto_20191114_1111'), + ] + + operations = [ + migrations.CreateModel( + name='Platform', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.SlugField(allow_unicode=True, unique=True, verbose_name='Name')), + ('base', models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Other', 'Other')], default='Linux', max_length=16, verbose_name='Base')), + ('charset', models.CharField(choices=[('utf8', 'UTF-8'), ('gbk', 'GBK')], default='utf8', max_length=8, verbose_name='Charset')), + ('meta', common.fields.model.JsonDictTextField(blank=True, null=True, verbose_name='Meta')), + ('internal', models.BooleanField(default=False, verbose_name='Internal')), + ('comment', models.TextField(blank=True, null=True, verbose_name='Comment')), + ], + options={ + 'verbose_name': 'Platform' + } + ), + migrations.RunPython(create_internal_platform) + ] diff --git a/jumpserver/jumpserver/apps/assets/migrations/0045_auto_20191206_1607.py b/jumpserver/jumpserver/apps/assets/migrations/0045_auto_20191206_1607.py new file mode 100644 index 0000000000000000000000000000000000000000..f51839289c5f41b32742178bf657604e87636928 --- /dev/null +++ b/jumpserver/jumpserver/apps/assets/migrations/0045_auto_20191206_1607.py @@ -0,0 +1,47 @@ +# Generated by Django 2.2.7 on 2019-12-06 08:07 + +import assets.models.asset +from django.db import migrations, models +import django.db.models.deletion + + +def migrate_platform_to_asset_type(apps, schema_editor): + asset_model = apps.get_model("assets", "Asset") + platform_model = apps.get_model("assets", "Platform") + db_alias = schema_editor.connection.alias + + platforms = platform_model.objects.using(db_alias).all() + platforms_map = {p.name: p for p in platforms} + for name, p in platforms_map.items(): + asset_model.objects.using(db_alias)\ + .filter(_platform=name)\ + .update(platform=p) + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0044_platform'), + ] + + operations = [ + migrations.RenameField( + model_name='asset', + old_name='platform', + new_name='_platform', + ), + migrations.AddField( + model_name='asset', + name='platform', + field=models.ForeignKey( + default=assets.models.asset.Platform.default, + on_delete=django.db.models.deletion.PROTECT, + related_name='assets', to='assets.Platform', + verbose_name='Platform'), + ), + migrations.RunPython(migrate_platform_to_asset_type), + migrations.RemoveField( + model_name='asset', + name='_platform', + ), + ] diff --git a/jumpserver/jumpserver/apps/assets/migrations/0046_auto_20191218_1705.py b/jumpserver/jumpserver/apps/assets/migrations/0046_auto_20191218_1705.py new file mode 100644 index 0000000000000000000000000000000000000000..af776eee28306107773104fdd400e6d7b6ccc91c --- /dev/null +++ b/jumpserver/jumpserver/apps/assets/migrations/0046_auto_20191218_1705.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.11 on 2019-12-18 09:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0045_auto_20191206_1607'), + ] + + operations = [ + migrations.AlterField( + model_name='systemuser', + name='protocol', + field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet'), ('vnc', 'vnc'), ('mysql', 'mysql')], default='ssh', max_length=16, verbose_name='Protocol'), + ), + ] diff --git a/jumpserver/jumpserver/apps/assets/models/asset.py b/jumpserver/jumpserver/apps/assets/models/asset.py index d6e09786e4a0ba61b9678dfbf82d02dcf86b325b..70e2abb2a305c5b408dd4cecda9451e11b98ce89 100644 --- a/jumpserver/jumpserver/apps/assets/models/asset.py +++ b/jumpserver/jumpserver/apps/assets/models/asset.py @@ -11,10 +11,12 @@ from collections import OrderedDict from django.db import models from django.utils.translation import ugettext_lazy as _ -from .utils import Connectivity +from common.fields.model import JsonDictTextField +from common.utils import lazyproperty from orgs.mixins.models import OrgModelMixin, OrgManager +from .utils import Connectivity -__all__ = ['Asset', 'ProtocolsMixin'] +__all__ = ['Asset', 'ProtocolsMixin', 'Platform'] logger = logging.getLogger(__name__) @@ -37,6 +39,13 @@ def default_node(): return None +class AssetManager(OrgManager): + def get_queryset(self): + return super().get_queryset().annotate( + platform_base=models.F('platform__base') + ) + + class AssetQuerySet(models.QuerySet): def active(self): return self.filter(is_active=True) @@ -119,6 +128,47 @@ class NodesRelationMixin: return nodes +class Platform(models.Model): + CHARSET_CHOICES = ( + ('utf8', 'UTF-8'), + ('gbk', 'GBK'), + ) + BASE_CHOICES = ( + ('Linux', 'Linux'), + ('Unix', 'Unix'), + ('MacOS', 'MacOS'), + ('BSD', 'BSD'), + ('Windows', 'Windows'), + ('Other', 'Other'), + ) + name = models.SlugField(verbose_name=_("Name"), unique=True, allow_unicode=True) + base = models.CharField(choices=BASE_CHOICES, max_length=16, default='Linux', verbose_name=_("Base")) + charset = models.CharField(default='utf8', choices=CHARSET_CHOICES, max_length=8, verbose_name=_("Charset")) + meta = JsonDictTextField(blank=True, null=True, verbose_name=_("Meta")) + internal = models.BooleanField(default=False, verbose_name=_("Internal")) + comment = models.TextField(blank=True, null=True, verbose_name=_("Comment")) + + @classmethod + def default(cls): + linux, created = cls.objects.get_or_create( + defaults={'name': 'Linux'}, name='Linux' + ) + return linux.id + + def is_windows(self): + return self.base.lower() in ('windows',) + + def is_unixlike(self): + return self.base.lower() in ("linux", "unix", "macos", "bsd") + + def __str__(self): + return self.name + + class Meta: + verbose_name = _("Platform") + # ordering = ('name',) + + class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin): # Important PLATFORM_CHOICES = ( @@ -138,9 +188,8 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin): choices=ProtocolsMixin.PROTOCOL_CHOICES, verbose_name=_('Protocol')) port = models.IntegerField(default=22, verbose_name=_('Port')) - protocols = models.CharField(max_length=128, default='ssh/22', blank=True, verbose_name=_("Protocols")) - platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES, default='Linux', verbose_name=_('Platform')) + platform = models.ForeignKey(Platform, default=Platform.default, on_delete=models.PROTECT, verbose_name=_("Platform"), related_name='assets') domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', verbose_name=_("Domain"), on_delete=models.SET_NULL) nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes")) is_active = models.BooleanField(default=True, verbose_name=_('Is active')) @@ -175,7 +224,7 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin): date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')) comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment')) - objects = OrgManager.from_queryset(AssetQuerySet)() + objects = AssetManager.from_queryset(AssetQuerySet)() _connectivity = None def __str__(self): @@ -190,20 +239,18 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin): return False, warning return True, warning + @lazyproperty + def platform_base(self): + return self.platform.base + def is_windows(self): - if self.platform in ("Windows", "Windows2016"): - return True - else: - return False + return self.platform.is_windows() def is_unixlike(self): - if self.platform not in ("Windows", "Windows2016", "Other"): - return True - else: - return False + return self.platform.is_unixlike() def is_support_ansible(self): - return self.has_protocol('ssh') and self.platform not in ("Other",) + return self.has_protocol('ssh') and self.platform_base not in ("Other",) @property def cpu_info(self): @@ -264,9 +311,9 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin): def as_tree_node(self, parent_node): from common.tree import TreeNode icon_skin = 'file' - if self.platform.lower() == 'windows': + if self.platform_base.lower() == 'windows': icon_skin = 'windows' - elif self.platform.lower() == 'linux': + elif self.platform_base.lower() == 'linux': icon_skin = 'linux' data = { 'id': str(self.id), @@ -283,7 +330,7 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin): 'hostname': self.hostname, 'ip': self.ip, 'protocols': self.protocols_as_list, - 'platform': self.platform, + 'platform': self.platform_base, } } } diff --git a/jumpserver/jumpserver/apps/assets/models/base.py b/jumpserver/jumpserver/apps/assets/models/base.py index 2e591c940f933212942a00da176147abc5e0f073..5d0f3cef11159c664cfa19ce3549fb09efde91e7 100644 --- a/jumpserver/jumpserver/apps/assets/models/base.py +++ b/jumpserver/jumpserver/apps/assets/models/base.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # +import io import os import uuid from hashlib import md5 @@ -11,14 +12,13 @@ from django.utils.translation import ugettext_lazy as _ from django.conf import settings from common.utils import ( - get_signer, ssh_key_string_to_obj, ssh_key_gen, get_logger + signer, ssh_key_string_to_obj, ssh_key_gen, get_logger ) from common.validators import alphanumeric from common import fields from orgs.mixins.models import OrgModelMixin from .utils import private_key_validator, Connectivity -signer = get_signer() logger = get_logger(__file__) @@ -41,6 +41,7 @@ class AssetUser(OrgModelMixin): ASSET_USER_CACHE_TIME = 3600 * 24 _prefer = "system_user" + _assets_amount = None @property def private_key_obj(self): @@ -76,6 +77,14 @@ class AssetUser(OrgModelMixin): i = '-'.join(str(self.id).split('-')[:3]) return i + def get_private_key(self): + if not self.private_key_obj: + return None + string_io = io.StringIO() + self.private_key_obj.write_private_key(string_io) + private_key = string_io.getvalue() + return private_key + def get_related_assets(self): assets = self.assets.all() return assets @@ -143,6 +152,8 @@ class AssetUser(OrgModelMixin): @property def assets_amount(self): + if self._assets_amount is not None: + return self._assets_amount cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id) cached = cache.get(cache_key) if not cached: diff --git a/jumpserver/jumpserver/apps/assets/models/gathered_user.py b/jumpserver/jumpserver/apps/assets/models/gathered_user.py index 282f9293a5efe90bb04b8ff5a5e392181a4afefa..d00021c568ad7505be800393b6137c6e50e1ca0e 100644 --- a/jumpserver/jumpserver/apps/assets/models/gathered_user.py +++ b/jumpserver/jumpserver/apps/assets/models/gathered_user.py @@ -12,13 +12,12 @@ __all__ = ['GatheredUser'] class GatheredUser(OrgModelMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_("Asset")) - username = models.CharField(max_length=32, blank=True, db_index=True, - verbose_name=_('Username')) + username = models.CharField(max_length=32, blank=True, db_index=True, verbose_name=_('Username')) present = models.BooleanField(default=True, verbose_name=_("Present")) - date_created = models.DateTimeField(auto_now_add=True, - verbose_name=_("Date created")) - date_updated = models.DateTimeField(auto_now=True, - verbose_name=_("Date updated")) + date_last_login = models.DateTimeField(null=True, verbose_name=_("Date last login")) + ip_last_login = models.CharField(max_length=39, default='', verbose_name=_("IP last login")) + date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created")) + date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated")) @property def hostname(self): diff --git a/jumpserver/jumpserver/apps/assets/models/user.py b/jumpserver/jumpserver/apps/assets/models/user.py index 443c32981fd36dadaca2b74fef68a66fbf05da95..053ca0f77ed174a882bc4ed8cb2acfc60a82558d 100644 --- a/jumpserver/jumpserver/apps/assets/models/user.py +++ b/jumpserver/jumpserver/apps/assets/models/user.py @@ -10,14 +10,13 @@ from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from django.core.validators import MinValueValidator, MaxValueValidator -from common.utils import get_signer +from common.utils import signer from .base import AssetUser from .asset import Asset __all__ = ['AdminUser', 'SystemUser'] logger = logging.getLogger(__name__) -signer = get_signer() class AdminUser(AssetUser): @@ -93,11 +92,13 @@ class SystemUser(AssetUser): PROTOCOL_RDP = 'rdp' PROTOCOL_TELNET = 'telnet' PROTOCOL_VNC = 'vnc' + PROTOCOL_MYSQL = 'mysql' PROTOCOL_CHOICES = ( (PROTOCOL_SSH, 'ssh'), (PROTOCOL_RDP, 'rdp'), (PROTOCOL_TELNET, 'telnet'), (PROTOCOL_VNC, 'vnc'), + (PROTOCOL_MYSQL, 'mysql'), ) LOGIN_AUTO = 'auto' @@ -134,6 +135,18 @@ class SystemUser(AssetUser): else: return False + @property + def is_need_cmd_filter(self): + return self.protocol not in [self.PROTOCOL_RDP, self.PROTOCOL_VNC] + + @property + def is_need_test_asset_connective(self): + return self.protocol not in [self.PROTOCOL_MYSQL] + + @property + def can_perm_to_asset(self): + return self.protocol not in [self.PROTOCOL_MYSQL] + @property def cmd_filter_rules(self): from .cmd_filter import CommandFilterRule diff --git a/jumpserver/jumpserver/apps/assets/serializers/asset.py b/jumpserver/jumpserver/apps/assets/serializers/asset.py index a24374c1349ca574a5646b1c35772332631f657c..c34fad77b40367e26b08ec26e330ef8802f49086 100644 --- a/jumpserver/jumpserver/apps/assets/serializers/asset.py +++ b/jumpserver/jumpserver/apps/assets/serializers/asset.py @@ -7,16 +7,13 @@ from django.utils.translation import ugettext_lazy as _ from orgs.mixins.serializers import BulkOrgResourceModelSerializer from common.serializers import AdaptedBulkListSerializer -from ..models import Asset, Node, Label -from ..const import ( - GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_PATTERN, - GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_ERROR_MSG -) +from ..models import Asset, Node, Label, Platform from .base import ConnectivitySerializer __all__ = [ 'AssetSerializer', 'AssetSimpleSerializer', - 'ProtocolsField', + 'ProtocolsField', 'PlatformSerializer', + 'AssetDetailSerializer', ] @@ -65,6 +62,9 @@ class ProtocolsField(serializers.ListField): class AssetSerializer(BulkOrgResourceModelSerializer): + platform = serializers.SlugRelatedField( + slug_field='name', queryset=Platform.objects.all(), label=_("Platform") + ) protocols = ProtocolsField(label=_('Protocols'), required=False) connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity")) @@ -96,22 +96,13 @@ class AssetSerializer(BulkOrgResourceModelSerializer): 'org_name': {'label': _('Org name')} } - @staticmethod - def validate_hostname(hostname): - pattern = GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_PATTERN - res = re.search(pattern, hostname) - if res is not None: - msg = GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_ERROR_MSG - raise serializers.ValidationError(msg) - return hostname - @classmethod def setup_eager_loading(cls, queryset): """ Perform necessary eager loading of data. """ queryset = queryset.prefetch_related( Prefetch('nodes', queryset=Node.objects.all().only('id')), Prefetch('labels', queryset=Label.objects.all().only('id')), - ).select_related('admin_user', 'domain') + ).select_related('admin_user', 'domain', 'platform') return queryset def compatible_with_old_protocol(self, validated_data): @@ -139,6 +130,21 @@ class AssetSerializer(BulkOrgResourceModelSerializer): return super().update(instance, validated_data) +class PlatformSerializer(serializers.ModelSerializer): + meta = serializers.DictField(required=False, allow_null=True) + + class Meta: + model = Platform + fields = [ + 'id', 'name', 'base', 'charset', + 'internal', 'meta', 'comment' + ] + + +class AssetDetailSerializer(AssetSerializer): + platform = PlatformSerializer(read_only=True) + + class AssetSimpleSerializer(serializers.ModelSerializer): connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity")) diff --git a/jumpserver/jumpserver/apps/assets/serializers/gathered_user.py b/jumpserver/jumpserver/apps/assets/serializers/gathered_user.py index 956c19c6bad8ab7aa9ce263f1c0de1f2da1d5751..c055e25bd04fedf5b027863df1c8912baac7554d 100644 --- a/jumpserver/jumpserver/apps/assets/serializers/gathered_user.py +++ b/jumpserver/jumpserver/apps/assets/serializers/gathered_user.py @@ -12,6 +12,7 @@ class GatheredUserSerializer(OrgResourceModelSerializerMixin): model = GatheredUser fields = [ 'id', 'asset', 'hostname', 'ip', 'username', + 'date_last_login', 'ip_last_login', 'present', 'date_created', 'date_updated' ] read_only_fields = fields diff --git a/jumpserver/jumpserver/apps/assets/serializers/system_user.py b/jumpserver/jumpserver/apps/assets/serializers/system_user.py index 648af01a718986e5c5872d1e7cb9283e51e4b4e5..e16a7a2712ba8c957921c87db9db79eef4a95652 100644 --- a/jumpserver/jumpserver/apps/assets/serializers/system_user.py +++ b/jumpserver/jumpserver/apps/assets/serializers/system_user.py @@ -4,15 +4,19 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ from common.serializers import AdaptedBulkListSerializer +from common.mixins.serializers import BulkSerializerMixin from common.utils import ssh_pubkey_gen from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from assets.models import Node from ..models import SystemUser -from ..const import ( - GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_PATTERN, - GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_ERROR_MSG -) from .base import AuthSerializer, AuthSerializerMixin +__all__ = [ + 'SystemUserSerializer', 'SystemUserAuthSerializer', + 'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer', + 'SystemUserNodeRelationSerializer', +] + class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): """ @@ -39,15 +43,6 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): 'created_by': {'read_only': True}, } - @staticmethod - def validate_name(name): - pattern = GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_PATTERN - res = re.search(pattern, name) - if res is not None: - msg = GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_ERROR_MSG - raise serializers.ValidationError(msg) - return name - def validate_auto_push(self, value): login_mode = self.initial_data.get("login_mode") protocol = self.initial_data.get("protocol") @@ -95,8 +90,12 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): def validate(self, attrs): username = attrs.get("username", "manual") + auto_gen_key = attrs.pop("auto_generate_key", False) protocol = attrs.get("protocol") - auto_gen_key = attrs.get("auto_generate_key", False) + + if protocol not in [SystemUser.PROTOCOL_RDP, SystemUser.PROTOCOL_SSH]: + return attrs + if auto_gen_key: password = SystemUser.gen_password() attrs["password"] = password @@ -111,7 +110,6 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer): public_key = ssh_pubkey_gen(private_key, password=password, username=username) attrs["public_key"] = public_key - attrs.pop("auto_generate_key", None) return attrs @classmethod @@ -125,6 +123,7 @@ class SystemUserAuthSerializer(AuthSerializer): """ 系统用户认证信息 """ + private_key = serializers.SerializerMethodField() class Meta: model = SystemUser @@ -133,6 +132,10 @@ class SystemUserAuthSerializer(AuthSerializer): "login_mode", "password", "private_key", ] + @staticmethod + def get_private_key(obj): + return obj.get_private_key() + class SystemUserSimpleSerializer(serializers.ModelSerializer): """ @@ -143,4 +146,43 @@ class SystemUserSimpleSerializer(serializers.ModelSerializer): fields = ('id', 'name', 'username') +class RelationMixin(BulkSerializerMixin, serializers.Serializer): + systemuser_display = serializers.ReadOnlyField() + + def get_field_names(self, declared_fields, info): + fields = super().get_field_names(declared_fields, info) + fields.extend(['systemuser', "systemuser_display"]) + return fields + + class Meta: + list_serializer_class = AdaptedBulkListSerializer + + +class SystemUserAssetRelationSerializer(RelationMixin, serializers.ModelSerializer): + asset_display = serializers.ReadOnlyField() + + class Meta(RelationMixin.Meta): + model = SystemUser.assets.through + fields = [ + 'id', "asset", "asset_display", + ] + + +class SystemUserNodeRelationSerializer(RelationMixin, serializers.ModelSerializer): + node_display = serializers.SerializerMethodField() + + class Meta(RelationMixin.Meta): + model = SystemUser.nodes.through + fields = [ + 'id', 'node', "node_display", + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.tree = Node.tree() + def get_node_display(self, obj): + if hasattr(obj, 'node_key'): + return self.tree.get_node_full_tag(obj.node_key) + else: + return obj.node.full_value diff --git a/jumpserver/jumpserver/apps/assets/signals_handler.py b/jumpserver/jumpserver/apps/assets/signals_handler.py index 594da38b794bb0149048100811ce430d11a84220..4f171d36fe9333efa820c57c4ee6c6b324d81095 100644 --- a/jumpserver/jumpserver/apps/assets/signals_handler.py +++ b/jumpserver/jumpserver/apps/assets/signals_handler.py @@ -10,6 +10,7 @@ from django.dispatch import receiver from common.utils import get_logger, timeit from common.decorator import on_transaction_commit from .models import Asset, SystemUser, Node, AuthBook +from .utils import TreeService from .tasks import ( update_assets_hardware_info_util, test_asset_connectivity_util, @@ -131,16 +132,21 @@ def on_asset_nodes_add(sender, instance=None, action='', model=None, pk_set=None if action != "post_add": return logger.debug("Assets node add signal recv: {}".format(action)) - queryset = model.objects.filter(pk__in=pk_set).values_list('id', flat=True) if model == Node: - nodes = queryset - assets = [instance] + nodes = model.objects.filter(pk__in=pk_set).values_list('key', flat=True) + assets = [instance.id] else: - nodes = [instance] - assets = queryset - # 节点资产发生变化时,将资产关联到节点关联的系统用户, 只关注新增的 + nodes = [instance.key] + assets = model.objects.filter(pk__in=pk_set).values_list('id', flat=True) + # 节点资产发生变化时,将资产关联到节点及祖先节点关联的系统用户, 只关注新增的 + nodes_ancestors_keys = set() + node_tree = TreeService.new() + for node in nodes: + ancestors_keys = node_tree.ancestors_ids(nid=node) + nodes_ancestors_keys.update(ancestors_keys) + system_users = SystemUser.objects.filter(nodes__key__in=nodes_ancestors_keys) + system_users_assets = defaultdict(set) - system_users = SystemUser.objects.filter(nodes__in=nodes) for system_user in system_users: system_users_assets[system_user].update(set(assets)) for system_user, _assets in system_users_assets.items(): diff --git a/jumpserver/jumpserver/apps/assets/tasks/const.py b/jumpserver/jumpserver/apps/assets/tasks/const.py index 61a9580edb13e339891638a15296994b9ba07749..5b7db13cdc4b0850eb0256c93162dbac82da954b 100644 --- a/jumpserver/jumpserver/apps/assets/tasks/const.py +++ b/jumpserver/jumpserver/apps/assets/tasks/const.py @@ -94,6 +94,13 @@ GATHER_ASSET_USERS_TASKS = [ "args": "database=passwd" }, }, + { + "name": "get last login", + "action": { + "module": "shell", + "args": "users=$(getent passwd | grep -v 'nologin' | grep -v 'shudown' | awk -F: '{ print $1 }');for i in $users;do last -F $i -1 | head -1 | grep -v '^$' | awk '{ print $1\"@\"$3\"@\"$5,$6,$7,$8 }';done" + } + } ] GATHER_ASSET_USERS_TASKS_WINDOWS = [ diff --git a/jumpserver/jumpserver/apps/assets/tasks/gather_asset_users.py b/jumpserver/jumpserver/apps/assets/tasks/gather_asset_users.py index efeecc25ed7326d66d80a8aedd15d973e6349c6b..7dfe0fb0198997f999734845a7729283e8d8feeb 100644 --- a/jumpserver/jumpserver/apps/assets/tasks/gather_asset_users.py +++ b/jumpserver/jumpserver/apps/assets/tasks/gather_asset_users.py @@ -2,9 +2,10 @@ import re from collections import defaultdict -from celery import shared_task +from celery import shared_task from django.utils.translation import ugettext as _ +from django.utils import timezone from orgs.utils import tmp_to_org from common.utils import get_logger @@ -19,19 +20,25 @@ ignore_login_shell = re.compile(r'nologin$|sync$|shutdown$|halt$') def parse_linux_result_to_users(result): - task_result = {} - for task_name, raw in result.items(): - res = raw.get('ansible_facts', {}).get('getent_passwd') - if res: - task_result = res - break - if not task_result or not isinstance(task_result, dict): - return [] - users = [] - for username, attr in task_result.items(): + users = defaultdict(dict) + users_result = result.get('gather host users', {})\ + .get('ansible_facts', {})\ + .get('getent_passwd') + if not isinstance(users_result, dict): + users_result = {} + for username, attr in users_result.items(): if ignore_login_shell.search(attr[-1]): continue - users.append(username) + users[username] = {} + last_login_result = result.get('get last login', {}).get('stdout_lines', []) + for line in last_login_result: + data = line.split('@') + if len(data) != 3: + continue + username, ip, dt = data + dt += ' +0800' + date = timezone.datetime.strptime(dt, '%b %d %H:%M:%S %Y %z') + users[username] = {"ip": ip, "date": date} return users @@ -45,7 +52,7 @@ def parse_windows_result_to_users(result): if not task_result: return [] - users = [] + users = {} for i in range(4): task_result.pop(0) @@ -55,7 +62,7 @@ def parse_windows_result_to_users(result): for line in task_result: user = space.split(line) if user[0]: - users.append(user[0]) + users[user[0]] = {} return users @@ -82,8 +89,12 @@ def add_asset_users(assets, results): with tmp_to_org(asset.org_id): GatheredUser.objects.filter(asset=asset, present=True)\ .update(present=False) - for username in users: + for username, data in users.items(): defaults = {'asset': asset, 'username': username, 'present': True} + if data.get("ip"): + defaults["ip_last_login"] = data["ip"] + if data.get("date"): + defaults["date_last_login"] = data["date"] GatheredUser.objects.update_or_create( defaults=defaults, asset=asset, username=username, ) diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/_admin_user_import_modal.html b/jumpserver/jumpserver/apps/assets/templates/assets/_admin_user_import_modal.html deleted file mode 100644 index a4afc1a14ab2d8309d109430569bd18444824515..0000000000000000000000000000000000000000 --- a/jumpserver/jumpserver/apps/assets/templates/assets/_admin_user_import_modal.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends '_import_modal.html' %} -{% load i18n %} - -{% block modal_title%}{% trans "Import admin user" %}{% endblock %} - -{% block import_modal_download_template_url %}{% url "api-assets:admin-user-list" %}{% endblock %} diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/_admin_user_update_modal.html b/jumpserver/jumpserver/apps/assets/templates/assets/_admin_user_update_modal.html deleted file mode 100644 index 9af051dd2d9eb4b910f5c8870089808ae6073673..0000000000000000000000000000000000000000 --- a/jumpserver/jumpserver/apps/assets/templates/assets/_admin_user_update_modal.html +++ /dev/null @@ -1,4 +0,0 @@ -{% extends '_update_modal.html' %} -{% load i18n %} - -{% block modal_title%}{% trans "Update admin user" %}{% endblock %} \ No newline at end of file diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/_asset_group_bulk_update_modal.html b/jumpserver/jumpserver/apps/assets/templates/assets/_asset_group_bulk_update_modal.html index 61ac04fa6b761b1d5abf37f20afaa480ec6559f9..7df6c4ede998a0666475879324c72069d66937b9 100644 --- a/jumpserver/jumpserver/apps/assets/templates/assets/_asset_group_bulk_update_modal.html +++ b/jumpserver/jumpserver/apps/assets/templates/assets/_asset_group_bulk_update_modal.html @@ -31,11 +31,11 @@
- +
{% endblock %} -{% block modal_confirm_id %}btn_asset_group_bulk_update{% endblock %} \ No newline at end of file +{% block modal_confirm_id %}btn_asset_group_bulk_update{% endblock %} diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/_asset_import_modal.html b/jumpserver/jumpserver/apps/assets/templates/assets/_asset_import_modal.html deleted file mode 100644 index 2460cb0538df5544580a9a0c5a968ebb19b0eec7..0000000000000000000000000000000000000000 --- a/jumpserver/jumpserver/apps/assets/templates/assets/_asset_import_modal.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends '_import_modal.html' %} -{% load i18n %} - -{% block modal_title%}{% trans "Import assets" %}{% endblock %} - -{% block import_modal_download_template_url %}{% url "api-assets:asset-list" %}{% endblock %} diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/_asset_list_modal.html b/jumpserver/jumpserver/apps/assets/templates/assets/_asset_list_modal.html index 4c6eb719931dc6958b8f1be3887100b2de8f4a8b..dea2c3e1e14974a1fc3ced8090809937ac9ba759 100644 --- a/jumpserver/jumpserver/apps/assets/templates/assets/_asset_list_modal.html +++ b/jumpserver/jumpserver/apps/assets/templates/assets/_asset_list_modal.html @@ -25,7 +25,7 @@
-
+
@@ -37,7 +37,7 @@
-
+
@@ -135,7 +135,8 @@ function initAssetModalTable() { ], lengthMenu: [[10, 25, 50], [10, 25, 50]], pageLength: 10, - select_style: assetModalOption.selectStyle + select_style: assetModalOption.selectStyle, + paging_numbers_length: 3 }; assetModalTable = jumpserver.initServerSideDataTable(options); if (assetModalOption.onModalTableDone) { @@ -189,6 +190,16 @@ function setAssetModalOptions(options) { assetModalOption = options; } +function initAssetTreeModel(selector) { + $(selector).parent().find(".select2-selection").on('click', function (e) { + if ($(e.target).attr('class') !== 'select2-selection__choice__remove'){ + e.preventDefault(); + e.stopPropagation(); + $("#asset_list_modal").modal(); + } + }) +} + $(document).ready(function(){ diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/_asset_update_modal.html b/jumpserver/jumpserver/apps/assets/templates/assets/_asset_update_modal.html deleted file mode 100644 index 68b2ff8db22c8ff81e477b9afd0e6bc117362e5e..0000000000000000000000000000000000000000 --- a/jumpserver/jumpserver/apps/assets/templates/assets/_asset_update_modal.html +++ /dev/null @@ -1,4 +0,0 @@ -{% extends '_update_modal.html' %} -{% load i18n %} - -{% block modal_title%}{% trans "Update assets" %}{% endblock %} diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/_node_detail_modal.html b/jumpserver/jumpserver/apps/assets/templates/assets/_node_detail_modal.html new file mode 100644 index 0000000000000000000000000000000000000000..f1f6f2ddacf367a98ed56eb90699c4df18fcec05 --- /dev/null +++ b/jumpserver/jumpserver/apps/assets/templates/assets/_node_detail_modal.html @@ -0,0 +1,68 @@ +{% extends '_modal.html' %} +{% load i18n %} +{% load static %} + +{% block modal_id %}node_detail_modal{% endblock %} + +{% block modal_title %}{% trans "Node detail" %}{% endblock %} + + +{% block modal_body %} + +
+
+ +
+

+
+
+ +
+
+
+ +
+

+
+
+
+ +
+

+
+
+
+ +
+

+
+
+
+ + + + +{% endblock %} + +{% block modal_button %} + +{% endblock %} diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/_node_tree.html b/jumpserver/jumpserver/apps/assets/templates/assets/_node_tree.html index 803c29c13788873d2da12154ad057e01f0f3cfdb..b4b5040b0acfa0d1b2b1fbb9469012c5b4d809e6 100644 --- a/jumpserver/jumpserver/apps/assets/templates/assets/_node_tree.html +++ b/jumpserver/jumpserver/apps/assets/templates/assets/_node_tree.html @@ -216,8 +216,9 @@ function OnRightClick(event, treeId, treeNode) { function showRMenu(type, x, y) { var offset = $("#tree-node-id").offset(); + var scrollTop = document.querySelector('.treebox').scrollTop; x -= offset.left; - y -= offset.top; + y -= offset.top + scrollTop; x += document.body.scrollLeft; y += document.body.scrollTop + document.documentElement.scrollTop; rMenu.css({"top":y+"px", "left":x+"px", "visibility":"visible"}); @@ -303,9 +304,24 @@ function defaultCallback(action) { return logging } +function toggle() { + if (show === 0) { + $("#split-left").hide(500, function () { + $("#split-right").attr("class", "col-lg-12"); + $("#toggle-icon").attr("class", "fa fa-angle-right fa-x"); + show = 1; + }); + } else { + $("#split-right").attr("class", "col-lg-9"); + $("#toggle-icon").attr("class", "fa fa-angle-left fa-x"); + $("#split-left").show(500); + show = 0; + } +} + $(document).ready(function () { - $('.treebox').css('height', window.innerHeight - 180); + $('.treebox').css('height', window.innerHeight - 60); }) .on('click', '.btn-show-current-asset', function(){ hideRMenu(); @@ -320,6 +336,9 @@ $(document).ready(function () { $('#show_current_asset').css('display', 'inline-block'); setCookie('show_current_asset', ''); location.reload(); +}).on('click', '.tree-toggle-btn', function (e) { + e.preventDefault(); + toggle(); }) diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/_system_user.html b/jumpserver/jumpserver/apps/assets/templates/assets/_system_user.html index 078877cfac104d4c8624baa1a25b9db9e4136442..d941bf20ae73ead54700cf312d30edc77b721d5c 100644 --- a/jumpserver/jumpserver/apps/assets/templates/assets/_system_user.html +++ b/jumpserver/jumpserver/apps/assets/templates/assets/_system_user.html @@ -2,10 +2,6 @@ {% load i18n %} {% load static %} {% load bootstrap3 %} -{% block custom_head_css_js %} - - -{% endblock %} {% block content %}
@@ -92,6 +88,7 @@ var auto_generate_key = '#'+'{{ form.auto_generate_key.id_for_label }}'; var password_id = '#' + '{{ form.password.id_for_label }}'; var private_key_id = '#' + '{{ form.private_key.id_for_label }}'; var auto_push_id = '#' + '{{ form.auto_push.id_for_label }}'; +var command_filter_block_id = '#command-filter-block'; var sudo_id = '#' + '{{ form.sudo.id_for_label }}'; var shell_id = '#' + '{{ form.shell.id_for_label }}'; @@ -99,13 +96,13 @@ function autoLoginModeProtocol() { // 协议+自动登录模式字段控制 $('#auth_title_id').removeClass('hidden'); var protocol = $(protocol_id + " option:selected").text(); - if (protocol === 'rdp') { + if (['rdp'].indexOf(protocol) !== -1) { authFieldsDisplay(); $(auto_generate_key).closest('.form-group').removeClass('hidden'); $(private_key_id).closest('.form-group').addClass('hidden'); $(password_id).closest('.form-group').removeClass('hidden'); $(auto_push_id).closest('.form-group').removeClass('hidden'); - $('#command-filter-block').addClass('hidden'); + $(command_filter_block_id).addClass('hidden'); $(sudo_id).closest('.form-group').addClass('hidden'); $(shell_id).closest('.form-group').addClass('hidden'); } @@ -115,7 +112,17 @@ function autoLoginModeProtocol() { $(private_key_id).closest('.form-group').addClass('hidden'); $(password_id).closest('.form-group').removeClass('hidden'); $(auto_push_id).closest('.form-group').addClass('hidden'); - $('#command-filter-block').addClass('hidden'); + $(command_filter_block_id).addClass('hidden'); + $(sudo_id).closest('.form-group').addClass('hidden'); + $(shell_id).closest('.form-group').addClass('hidden'); + } + else if (protocol === 'mysql'){ + $('.auth-fields').removeClass('hidden'); + $(auto_generate_key).closest('.form-group').addClass('hidden'); + $(private_key_id).closest('.form-group').addClass('hidden'); + $(password_id).closest('.form-group').removeClass('hidden'); + $(auto_push_id).closest('.form-group').addClass('hidden'); + $(command_filter_block_id).removeClass('hidden'); $(sudo_id).closest('.form-group').addClass('hidden'); $(shell_id).closest('.form-group').addClass('hidden'); } @@ -125,7 +132,7 @@ function autoLoginModeProtocol() { $(private_key_id).closest('.form-group').addClass('hidden'); $(password_id).closest('.form-group').removeClass('hidden'); $(auto_push_id).closest('.form-group').addClass('hidden'); - $('#command-filter-block').removeClass('hidden'); + $(command_filter_block_id).removeClass('hidden'); $(sudo_id).closest('.form-group').addClass('hidden'); $(shell_id).closest('.form-group').addClass('hidden'); } @@ -135,7 +142,7 @@ function autoLoginModeProtocol() { $(private_key_id).closest('.form-group').removeClass('hidden'); $(password_id).closest('.form-group').removeClass('hidden'); $(auto_push_id).closest('.form-group').removeClass('hidden'); - $('#command-filter-block').removeClass('hidden'); + $(command_filter_block_id).removeClass('hidden'); $(sudo_id).closest('.form-group').removeClass('hidden'); $(shell_id).closest('.form-group').removeClass('hidden'); } @@ -145,23 +152,33 @@ function manualLoginModeProtocol() { // 协议+手动登录模式字段控制 $('#auth_title_id').addClass('hidden'); var protocol = $(protocol_id + " option:selected").text(); - if (protocol === 'rdp') { + if (['rdp'].indexOf(protocol) !== -1) { $('.auth-fields').addClass('hidden'); $(auto_generate_key).closest('.form-group').addClass('hidden'); $(password_id).closest('.form-group').addClass('hidden'); $(private_key_id).closest('.form-group').addClass('hidden'); $(auto_push_id).closest('.form-group').addClass('hidden'); - $('#command-filter-block').addClass('hidden'); + $(command_filter_block_id).addClass('hidden'); $(sudo_id).closest('.form-group').addClass('hidden'); $(shell_id).closest('.form-group').addClass('hidden'); } - else if (protocol === 'vnc') { + else if (protocol === 'vnc'){ $('.auth-fields').addClass('hidden'); $(auto_generate_key).closest('.form-group').addClass('hidden'); $(password_id).closest('.form-group').addClass('hidden'); $(private_key_id).closest('.form-group').addClass('hidden'); $(auto_push_id).closest('.form-group').addClass('hidden'); - $('#command-filter-block').addClass('hidden'); + $(command_filter_block_id).addClass('hidden'); + $(sudo_id).closest('.form-group').addClass('hidden'); + $(shell_id).closest('.form-group').addClass('hidden'); + } + else if (protocol === 'mysql'){ + $('.auth-fields').addClass('hidden'); + $(auto_generate_key).closest('.form-group').addClass('hidden'); + $(password_id).closest('.form-group').addClass('hidden'); + $(private_key_id).closest('.form-group').addClass('hidden'); + $(auto_push_id).closest('.form-group').addClass('hidden'); + $(command_filter_block_id).removeClass('hidden'); $(sudo_id).closest('.form-group').addClass('hidden'); $(shell_id).closest('.form-group').addClass('hidden'); } @@ -171,7 +188,7 @@ function manualLoginModeProtocol() { $(password_id).closest('.form-group').addClass('hidden'); $(private_key_id).closest('.form-group').addClass('hidden'); $(auto_push_id).closest('.form-group').addClass('hidden'); - $('#command-filter-block').removeClass('hidden'); + $(command_filter_block_id).removeClass('hidden'); $(sudo_id).closest('.form-group').addClass('hidden'); $(shell_id).closest('.form-group').addClass('hidden'); } @@ -181,7 +198,7 @@ function manualLoginModeProtocol() { $(password_id).closest('.form-group').addClass('hidden'); $(private_key_id).closest('.form-group').addClass('hidden'); $(auto_push_id).closest('.form-group').addClass('hidden'); - $('#command-filter-block').removeClass('hidden'); + $(command_filter_block_id).removeClass('hidden'); $(sudo_id).closest('.form-group').removeClass('hidden'); $(shell_id).closest('.form-group').removeClass('hidden'); } @@ -247,4 +264,4 @@ $(document).ready(function () { }) -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/_system_user_import_modal.html b/jumpserver/jumpserver/apps/assets/templates/assets/_system_user_import_modal.html deleted file mode 100644 index b8687d696920da7a16876ac05290f6559363046d..0000000000000000000000000000000000000000 --- a/jumpserver/jumpserver/apps/assets/templates/assets/_system_user_import_modal.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends '_import_modal.html' %} -{% load i18n %} - -{% block modal_title%}{% trans "Import system user" %}{% endblock %} - -{% block import_modal_download_template_url %}{% url "api-assets:system-user-list" %}{% endblock %} diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/_system_user_update_modal.html b/jumpserver/jumpserver/apps/assets/templates/assets/_system_user_update_modal.html deleted file mode 100644 index 9e2920e6ab39cc91ef0841772a659b01d5753106..0000000000000000000000000000000000000000 --- a/jumpserver/jumpserver/apps/assets/templates/assets/_system_user_update_modal.html +++ /dev/null @@ -1,4 +0,0 @@ -{% extends '_update_modal.html' %} -{% load i18n %} - -{% block modal_title%}{% trans "Update system user" %}{% endblock %} \ No newline at end of file diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/admin_user_assets.html b/jumpserver/jumpserver/apps/assets/templates/assets/admin_user_assets.html index a2a52e0e25c239d093df493870f9246a0d00af53..e77cbb68936f0765ad2d543063f1c0346377803c 100644 --- a/jumpserver/jumpserver/apps/assets/templates/assets/admin_user_assets.html +++ b/jumpserver/jumpserver/apps/assets/templates/assets/admin_user_assets.html @@ -2,11 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} - {% block content %}
diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/admin_user_create_update.html b/jumpserver/jumpserver/apps/assets/templates/assets/admin_user_create_update.html index c94a67a9e48e867c350d73522787ed9f3c6741ea..213f038d1af9c5e9c60da9d3904052ec6a9370bc 100644 --- a/jumpserver/jumpserver/apps/assets/templates/assets/admin_user_create_update.html +++ b/jumpserver/jumpserver/apps/assets/templates/assets/admin_user_create_update.html @@ -2,10 +2,6 @@ {% load i18n %} {% load static %} {% load bootstrap3 %} -{% block custom_head_css_js %} - - -{% endblock %} {% block content %}
@@ -87,4 +83,4 @@ $(document).ready(function () { }) }) -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/admin_user_detail.html b/jumpserver/jumpserver/apps/assets/templates/assets/admin_user_detail.html index 66480a2cb0502e95eb031d2645e3a6dc3fca96af..e3f618d9382db897d8fa0f948cdd28513028ee14 100644 --- a/jumpserver/jumpserver/apps/assets/templates/assets/admin_user_detail.html +++ b/jumpserver/jumpserver/apps/assets/templates/assets/admin_user_detail.html @@ -2,11 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} - {% block content %}
diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/admin_user_list.html b/jumpserver/jumpserver/apps/assets/templates/assets/admin_user_list.html index 7a4b2a2dc422d05f6fa8134e3ffd36df0adba7d6..93477474c9289104703cc3794de43083c59b2621 100644 --- a/jumpserver/jumpserver/apps/assets/templates/assets/admin_user_list.html +++ b/jumpserver/jumpserver/apps/assets/templates/assets/admin_user_list.html @@ -1,34 +1,11 @@ {% extends '_base_list.html' %} {% load i18n static %} {% block help_message %} -
{% trans 'Admin users are asset (charged server) on the root, or have NOPASSWD: ALL sudo permissions users, '%} {% trans 'Jumpserver users of the system using the user to `push system user`, `get assets hardware information`, etc. '%} -
{% endblock %} {% block table_search %} - + {% include '_csv_import_export.html' %} {% endblock %} {% block table_container %} @@ -44,9 +21,6 @@
-{# #} -{# #} -{# #} @@ -54,8 +28,6 @@
{% trans 'Name' %} {% trans 'Username' %} {% trans 'Asset' %}{% trans 'Reachable' %}{% trans 'Unreachable' %}{% trans 'Ratio' %}{% trans 'Comment' %} {% trans 'Action' %}
- {% include 'assets/_admin_user_import_modal.html' %} - {% include 'assets/_admin_user_update_modal.html' %} {% endblock %} {% block content_bottom_left %}{% endblock %} {% block custom_foot_js %} @@ -79,17 +51,16 @@ function initTable() { columns: [ {data: function(){return ""}}, {data: "name"}, {data: "username" }, {data: "assets_amount", orderable: false}, {#{data: "connectivity_amount"}, {data: "connectivity_amount"}, {data: "connectivity_amount"},#} - {data: "comment"}, {data: "id", orderable: false, width: "100px"} + {data: "comment"}, {data: "id", orderable: false, width: "120px"} ] }; - admin_user_table = jumpserver.initServerSideDataTable(options); - return admin_user_table + return jumpserver.initServerSideDataTable(options); } $(document).ready(function(){ - initTable(); + admin_user_table = initTable(); + initCsvImportExport(admin_user_table, "{% trans "Admin user" %}") }) - .on('click', '.btn_admin_user_delete', function () { var $this = $(this); var $data_table = $("#admin_user_list_table").DataTable(); @@ -102,69 +73,5 @@ $(document).ready(function(){ }, 3000); }) -.on('click', '.btn_export', function(){ - var admin_users = admin_user_table.selected; - var data = { - 'resources': admin_users - }; - var search = $("input[type='search']").val(); - var props = { - method: "POST", - body: JSON.stringify(data), - success_url: "{% url 'api-assets:admin-user-list' %}", - format: "csv", - params: { - search: search - } - }; - APIExportData(props); -}).on('click', '#btn_import_confirm',function () { - var url = "{% url 'api-assets:admin-user-list' %}"; - var file = document.getElementById('id_file').files[0]; - if(!file){ - toastr.error("{% trans "Please select file" %}"); - return - } - var data_table = $('#admin_user_list_table').DataTable(); - APIImportData({ - url: url, - method: "POST", - body: file, - data_table: data_table - }); -}) -.on('click', '#download_update_template', function () { - var admin_users = admin_user_table.selected; - var data = { - 'resources': admin_users - }; - var search = $("input[type='search']").val(); - var props = { - method: "POST", - body: JSON.stringify(data), - success_url: "{% url 'api-assets:admin-user-list' %}?format=csv&template=update", - format: 'csv', - params: { - search: search - } - }; - APIExportData(props); -}) -.on('click', '#btn_update_confirm', function () { - var file = document.getElementById('update_file').files[0]; - if(!file){ - toastr.error("{% trans "Please select file" %}"); - return - } - var url = "{% url 'api-assets:admin-user-list' %}"; - var data_table = $('#admin_user_list_table').DataTable(); - - APIImportData({ - url: url, - method: "PUT", - body: file, - data_table: data_table - }); -}) {% endblock %} diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/asset_bulk_update.html b/jumpserver/jumpserver/apps/assets/templates/assets/asset_bulk_update.html index 08df48e91ed1df65e140ce415bb8b1b326cbb5b6..207d5eb526fc87a5581c6e6af0334845fd545ea2 100644 --- a/jumpserver/jumpserver/apps/assets/templates/assets/asset_bulk_update.html +++ b/jumpserver/jumpserver/apps/assets/templates/assets/asset_bulk_update.html @@ -32,13 +32,7 @@ {% endblock %} diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/asset_list.html b/jumpserver/jumpserver/apps/assets/templates/assets/asset_list.html index fcc17c87dce627b6eda105f5f5f80e4f7b6a7d53..2b990619c0d6d27a546455b3fd92b02d2a13ccc7 100644 --- a/jumpserver/jumpserver/apps/assets/templates/assets/asset_list.html +++ b/jumpserver/jumpserver/apps/assets/templates/assets/asset_list.html @@ -1,132 +1,54 @@ -{% extends 'base.html' %} +{% extends '_base_asset_tree_list.html' %} {% load static %} {% load i18n %} {% block help_message %} -
-{# 左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的,右侧是属于该节点下的资产#} {% trans 'The left side is the asset tree, right click to create, delete, and change the tree node, authorization asset is also organized as a node, and the right side is the asset under that node' %} -
{% endblock %} -{% block custom_head_css_js %} - -{# #} - - - - -{% endblock %} - -{% block content %} -
-
-
- {% include 'assets/_node_tree.html' %} -
-
-
-
- -
-
-
- - -
- - -
- - - - - - - - - - - - - -
{% trans 'Hostname' %}{% trans 'IP' %}{% trans 'Hardware' %}{% trans 'Reachable' %}{% trans 'Action' %}
-
-
- -
- -
-
-
+{% block table_container %} + + {% include '_csv_import_export.html' %} +
+ + +
+ + + + + + + + + + + + + +
{% trans 'Hostname' %}{% trans 'IP' %}{% trans 'Hardware' %}{% trans 'Reachable' %}{% trans 'Action' %}
+
+
+ +
+
-
- -{% include 'assets/_asset_update_modal.html' %} -{% include 'assets/_asset_import_modal.html' %} {% include 'assets/_asset_list_modal.html' %} +{% include 'assets/_node_detail_modal.html' %} {% endblock %} {% block custom_foot_js %} @@ -151,9 +73,9 @@ function initTable() { }}, {targets: 4, createdCell: function (td, cellData, rowData) { var innerHtml = ""; - if (cellData.status == 1) { + if (cellData.status === 1) { innerHtml = '' - } else if (cellData.status == 0) { + } else if (cellData.status === 0) { innerHtml = '' } else { innerHtml = '' @@ -177,7 +99,7 @@ function initTable() { data: "connectivity", orderable: false, width: '60px' - }, {data: "id", orderable: false, width: "100px"} + }, {data: "id", orderable: false, width: "120px"} ], op_html: $('#actions').html() }; @@ -198,26 +120,12 @@ function initTree() {
  • +
  • + ` }) } - -function toggle() { - if (show === 0) { - $("#split-left").hide(500, function () { - $("#split-right").attr("class", "col-lg-12"); - $("#toggle-icon").attr("class", "fa fa-angle-right fa-x"); - show = 1; - }); - } else { - $("#split-right").attr("class", "col-lg-9"); - $("#toggle-icon").attr("class", "fa fa-angle-left fa-x"); - $("#split-left").show(500); - show = 0; - } -} - function onNodeSelected(event, treeNode) { current_node = treeNode; current_node_id = treeNode.meta.node.id; @@ -258,7 +166,8 @@ function onAssetModalConfirmAddAssetToNode(table) { } $(document).ready(function(){ - initTable(); + asset_table = initTable(); + initCsvImportExport(asset_table, "{% trans "Asset" %}"); initTree(); if(getCookie('show_current_asset') === '1'){ @@ -279,81 +188,6 @@ $(document).ready(function(){ $("#asset_list_table_filter input").val(val); asset_table.search(val).draw(); }) -.on('click', '.btn_export', function () { - var assets = asset_table.selected; - var data = { - 'resources': assets - }; - var search = $("input[type='search']").val(); - var props = { - method: "POST", - body: JSON.stringify(data), - success_url: "{% url 'api-assets:asset-list' %}", - format: 'csv', - params: { - search: search, - node_id: current_node_id || '', - show_current_asset: getCookie('show_current_asset') - } - }; - APIExportData(props); -}) -.on('click', '#btn_import_confirm', function () { - var file = document.getElementById('id_file').files[0]; - if(!file){ - toastr.error("{% trans "Please select file" %}"); - return - } - var url = "{% url 'api-assets:asset-list' %}"; - if (current_node_id){ - url = setUrlParam(url, 'node_id', current_node_id); - } - var data_table = $('#asset_list_table').DataTable(); - - APIImportData({ - url: url, - method: "POST", - body: file, - data_table: data_table - }); -}) -.on('click', '#download_update_template', function () { - var assets = asset_table.selected; - var data = { - 'resources': assets - }; - var search = $("input[type='search']").val(); - var props = { - method: "POST", - body: JSON.stringify(data), - success_url: "{% url 'api-assets:asset-list' %}?format=csv&template=update", - format: 'csv', - params: { - search: search, - node_id: current_node_id || '' - } - }; - APIExportData(props); -}) -.on('click', '#btn_update_confirm', function () { - var file = document.getElementById('update_file').files[0]; - if(!file){ - toastr.error("{% trans "Please select file" %}"); - return - } - var url = "{% url 'api-assets:asset-list' %}"; - if (current_node_id){ - url = setUrlParam(url, 'node_id', current_node_id); - } - var data_table = $('#asset_list_table').DataTable(); - - APIImportData({ - url: url, - method: "PUT", - body: file, - data_table: data_table - }); -}) .on('click', '.btn-create-asset', function () { var url = "{% url 'assets:asset-create' %}"; if (current_node_id) { @@ -382,8 +216,9 @@ $(document).ready(function(){ var data = { 'resources': id_list }; - function refreshPage() { - setTimeout( function () {window.location.reload();}, 300); + + function reloadTable() { + asset_table.ajax.reload(); } function doDeactive() { @@ -396,7 +231,7 @@ $(document).ready(function(){ url: the_url, method: 'PATCH', body: JSON.stringify(data), - success: refreshPage + success: reloadTable }); } function doActive() { @@ -409,7 +244,7 @@ $(document).ready(function(){ url: the_url, method: 'PATCH', body: JSON.stringify(data), - success: refreshPage + success: reloadTable }); } function doDelete() { @@ -431,7 +266,7 @@ $(document).ready(function(){ success: function () { var msg = "{% trans 'Asset Deleted.' %}"; swal("{% trans 'Asset Delete' %}", msg, "success"); - refreshPage(); + reloadTable(); }, flash_message: false, }); @@ -478,16 +313,12 @@ $(document).ready(function(){ 'assets': id_list }; - var success = function () { - asset_table.ajax.reload() - }; var url = "{% url 'api-assets:node-remove-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id); - requestApi({ 'url': url, 'method': 'PUT', 'body': JSON.stringify(data), - 'success': success + 'success': reloadTable }) } @@ -550,6 +381,30 @@ $(document).ready(function(){ flash_message: false }); +}).on('click', '#menu_node_detail', function(e) { + e.preventDefault(); + var the_url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}"; + the_url = the_url.replace("{{ DEFAULT_PK }}", current_node_id); + function drawingNodeDetailModal(data){ + $('#id_node_detail_id_view').html(data['id']); + $('#id_node_detail_name_view').html(data['name']); + $('#id_node_detail_full_name_view').html(data['full_value']); + $('#id_node_detail_key_view').html(data['key']); + $('#node_detail_modal').modal(); + } + function error(data) { + alert(data) + } + function success(data) { + drawingNodeDetailModal(data) + } + requestApi({ + url: the_url, + error: error, + method: 'GET', + success: success, + flash_message: false + }); }) diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/cmd_filter_detail.html b/jumpserver/jumpserver/apps/assets/templates/assets/cmd_filter_detail.html index 8ef5d5b757603876468293a99ac2ff0cbf4037ae..24e19225394e869363eb4a161f8ff1a5ea7f9251 100644 --- a/jumpserver/jumpserver/apps/assets/templates/assets/cmd_filter_detail.html +++ b/jumpserver/jumpserver/apps/assets/templates/assets/cmd_filter_detail.html @@ -2,11 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} - {% block content %}
    diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/cmd_filter_list.html b/jumpserver/jumpserver/apps/assets/templates/assets/cmd_filter_list.html index 0582363c4ef678f52bac62ea132e0e06d72afa25..1d98d5500b1df6bca7704b5e131bc2799d15e762 100644 --- a/jumpserver/jumpserver/apps/assets/templates/assets/cmd_filter_list.html +++ b/jumpserver/jumpserver/apps/assets/templates/assets/cmd_filter_list.html @@ -2,14 +2,12 @@ {% load i18n static %} {% block table_search %}{% endblock %} {% block help_message %} -
    {% trans 'System user bound some command filter, each command filter has some rules,'%} {% trans 'When user login asset with this system user, then run a command,' %} {% trans 'The command will be filter by rules, higher priority rule run first,' %} {% trans 'When a rule matched, if rule action is allow, then allow command execute,' %} {% trans 'else if action is deny, then command with be deny,' %} {% trans 'else match next rule, if none matched, allowed' %} -
    {% endblock %} {% block table_container %}
    @@ -64,7 +62,7 @@ function initTable() { columns: [ {data: "id"}, {data: "name" }, {data: "rules", orderable: false}, {data: "system_users", orderable: false}, {data: "comment"}, - {data: "id", orderable: false, width: "100px"} + {data: "id", orderable: false, width: "120px"} ], op_html: $('#actions').html() }; diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/cmd_filter_rule_create_update.html b/jumpserver/jumpserver/apps/assets/templates/assets/cmd_filter_rule_create_update.html index 2edaa97dc978628c412e3b60bdd31f8f00024853..21279b4100766572df042b8e91ce8ee48471309f 100644 --- a/jumpserver/jumpserver/apps/assets/templates/assets/cmd_filter_rule_create_update.html +++ b/jumpserver/jumpserver/apps/assets/templates/assets/cmd_filter_rule_create_update.html @@ -2,10 +2,6 @@ {% load i18n %} {% load static %} {% load bootstrap3 %} -{% block custom_head_css_js %} - - -{% endblock %} {% block content %}
    @@ -91,4 +87,4 @@ $(document).ready(function(){ formSubmit(props); }) -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/cmd_filter_rule_list.html b/jumpserver/jumpserver/apps/assets/templates/assets/cmd_filter_rule_list.html index 4bdf5ff2b9c7bf013315dd10eae03a2c56cc6e3d..6076cb2ac72d6f5300b353faeef6942659fe920c 100644 --- a/jumpserver/jumpserver/apps/assets/templates/assets/cmd_filter_rule_list.html +++ b/jumpserver/jumpserver/apps/assets/templates/assets/cmd_filter_rule_list.html @@ -2,11 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} - {% block content %}
    diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/domain_create_update.html b/jumpserver/jumpserver/apps/assets/templates/assets/domain_create_update.html index 96bfd02aa353f70084032623ec3ee27ba92052f6..39939c8ca67f24d3dc3373849599834474198d57 100644 --- a/jumpserver/jumpserver/apps/assets/templates/assets/domain_create_update.html +++ b/jumpserver/jumpserver/apps/assets/templates/assets/domain_create_update.html @@ -25,9 +25,7 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/domain_detail.html b/jumpserver/jumpserver/apps/assets/templates/assets/domain_detail.html index c05e0ed803bee472d5de3142d9fdbf7c6089f671..5b01c30fea2b2aad16a6ccc9155904b08e5de5d2 100644 --- a/jumpserver/jumpserver/apps/assets/templates/assets/domain_detail.html +++ b/jumpserver/jumpserver/apps/assets/templates/assets/domain_detail.html @@ -2,11 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} - {% block content %}
    @@ -138,4 +133,4 @@ $(document).ready(function(){ }) ; -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/domain_gateway_list.html b/jumpserver/jumpserver/apps/assets/templates/assets/domain_gateway_list.html index 79636a2bcf5c2ecad25444a752df4770c0e3e756..8917ef810b9148622ca57e92d219cb47653017e9 100644 --- a/jumpserver/jumpserver/apps/assets/templates/assets/domain_gateway_list.html +++ b/jumpserver/jumpserver/apps/assets/templates/assets/domain_gateway_list.html @@ -2,11 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} - {% block content %}
    diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/domain_list.html b/jumpserver/jumpserver/apps/assets/templates/assets/domain_list.html index 2b82826a072ff4c7c4b36a09c307e8f8b22cac6a..623f1bea251425df61dd68d842111ed1e0821fca 100644 --- a/jumpserver/jumpserver/apps/assets/templates/assets/domain_list.html +++ b/jumpserver/jumpserver/apps/assets/templates/assets/domain_list.html @@ -3,13 +3,9 @@ {% block table_search %}{% endblock %} {% block help_message %} -
    -{# 网域功能是为了解决部分环境(如:混合云)无法直接连接而新增的功能,原理是通过网关服务器进行跳转登录。
    #} -{# JMS => 网域网关 => 目标资产#} {% trans 'The domain function is added to address the fact that some environments (such as the hybrid cloud) cannot be connected directly by jumping on the gateway server.' %}
    {% trans 'JMS => Domain gateway => Target assets' %} -
    {% endblock %} {% block table_container %} @@ -59,7 +55,7 @@ function initTable() { ajax_url: '{% url "api-assets:domain-list" %}', columns: [ {data: "id"}, {data: "name" }, {data: "asset_count", orderable: false }, - {data: "gateway_count", orderable: false }, {data: "comment" }, {data: "id", orderable: false, width: "100px"} + {data: "gateway_count", orderable: false }, {data: "comment" }, {data: "id", orderable: false, width: "120px"} ], op_html: $('#actions').html() }; diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/gateway_create_update.html b/jumpserver/jumpserver/apps/assets/templates/assets/gateway_create_update.html index 315302b6f7eec884e9ef5cc4111ca54be6d64f20..43dc07b1b212dd44fa3f3284f1ec57619193aa81 100644 --- a/jumpserver/jumpserver/apps/assets/templates/assets/gateway_create_update.html +++ b/jumpserver/jumpserver/apps/assets/templates/assets/gateway_create_update.html @@ -2,10 +2,6 @@ {% load i18n %} {% load static %} {% load bootstrap3 %} -{% block custom_head_css_js %} - - -{% endblock %} {% block content %}
    @@ -125,4 +121,4 @@ $(document).ready(function(){ protocolChange(); }); -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/label_create_update.html b/jumpserver/jumpserver/apps/assets/templates/assets/label_create_update.html index 41646003c4c94e941d028134f98a8a2d881a265a..62732d6ba6bfe1d96cf99b979c8d97043e57f698 100644 --- a/jumpserver/jumpserver/apps/assets/templates/assets/label_create_update.html +++ b/jumpserver/jumpserver/apps/assets/templates/assets/label_create_update.html @@ -29,9 +29,7 @@ $(document).ready(function () { $('.select2').select2({ closeOnSelect: false }) -}).on('click', '.select2-selection__rendered', function (e) { - e.preventDefault(); - $("#asset_list_modal").modal(); + initAssetTreeModel("#id_assets"); }) .on("submit", "form", function (evt) { evt.preventDefault(); @@ -55,4 +53,4 @@ $(document).ready(function () { formSubmit(props); }) -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/label_list.html b/jumpserver/jumpserver/apps/assets/templates/assets/label_list.html index 9ab735f7a32154f6b46e483a5d60d9d3fd384731..104e5820b15df7159a8ded23d0f7ef4646b65811 100644 --- a/jumpserver/jumpserver/apps/assets/templates/assets/label_list.html +++ b/jumpserver/jumpserver/apps/assets/templates/assets/label_list.html @@ -45,7 +45,7 @@ function initTable() { columns: [ {data: "id"}, {data: "name" }, {data: "value" }, {data: "asset_count", orderable: false}, - {data: "id", orderable: false, width: "100px"} + {data: "id", orderable: false, width: "120px"} ], op_html: $('#actions').html() }; diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/platform_create_update.html b/jumpserver/jumpserver/apps/assets/templates/assets/platform_create_update.html new file mode 100644 index 0000000000000000000000000000000000000000..e130e9e97018eab0506d40aab12f8b57db1f8fbd --- /dev/null +++ b/jumpserver/jumpserver/apps/assets/templates/assets/platform_create_update.html @@ -0,0 +1,79 @@ +{% extends '_base_create_update.html' %} {% load static %} {% load bootstrap3 %} +{% load i18n %} + +{% block form %} +
    + {% csrf_token %} + {% bootstrap_field form.name layout="horizontal" %} + {% bootstrap_field form.base layout="horizontal" %} +
    + +
    + {% bootstrap_field form.comment layout="horizontal" %} + +
    +
    +
    + + +
    +
    +
    +{% endblock %} + +{% block custom_foot_js %} + +{% endblock %} + diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/platform_detail.html b/jumpserver/jumpserver/apps/assets/templates/assets/platform_detail.html new file mode 100644 index 0000000000000000000000000000000000000000..86d7f93e1945f98c12df7f0eb0cc336e4a8eb389 --- /dev/null +++ b/jumpserver/jumpserver/apps/assets/templates/assets/platform_detail.html @@ -0,0 +1,75 @@ +{% extends 'base.html' %} +{% load static %} +{% load i18n %} + +{% block content %} +
    +
    +
    +
    + +
    +
    +
    +
    + {{ object.name }} +
    + + + + + + + + + + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    {% trans 'Name' %}:{{ object.name }}
    {% trans 'Base platform' %}:{{ object.base }}
    {% trans 'Charset' %}:{{ object.charset }}
    {% trans 'Meta' %}:{{ object.meta }}
    {% trans 'Comment' %}:{{ object.comment }}
    +
    +
    +
    +
    +
    +
    +
    +
    +{% endblock %} +{% block content_bottom_left %}{% endblock %} +{% block custom_foot_js %} +{% endblock %} diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/platform_list.html b/jumpserver/jumpserver/apps/assets/templates/assets/platform_list.html new file mode 100644 index 0000000000000000000000000000000000000000..cb1eef5bc9d88fb417fa77b263908d07cb10e0a5 --- /dev/null +++ b/jumpserver/jumpserver/apps/assets/templates/assets/platform_list.html @@ -0,0 +1,75 @@ +{% extends '_base_list.html' %} +{% load i18n static %} +{% block table_search %} +{% endblock %} + +{% block table_container %} + + + + + + + + + + + + + +
    + + {% trans 'Name' %}{% trans 'Base platform' %}{% trans 'Comment' %}{% trans 'Action' %}
    +{% endblock %} +{% block content_bottom_left %}{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/system_user_assets.html b/jumpserver/jumpserver/apps/assets/templates/assets/system_user_assets.html index 4229d12ab0b72f66e6e7e015813a1c5205d98c79..ec024c1a74f019ac730c341a11d1e4cc31234e3b 100644 --- a/jumpserver/jumpserver/apps/assets/templates/assets/system_user_assets.html +++ b/jumpserver/jumpserver/apps/assets/templates/assets/system_user_assets.html @@ -4,9 +4,13 @@ {% load i18n %} {% block custom_head_css_js %} - - + {% endblock %} + {% block content %}
    @@ -98,15 +102,8 @@ - - {% for node in system_user.nodes.all|sort %} - - {{ node.full_value }} - - - - - {% endfor %} + +
    @@ -120,91 +117,115 @@ {% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/system_user_detail.html b/jumpserver/jumpserver/apps/assets/templates/assets/system_user_detail.html index 9c0557fde41b1ba6ac4e4f15faa249215253ec84..b9d6e5c0b4ef7141b4ec33b5d0732b5a9a07d4de 100644 --- a/jumpserver/jumpserver/apps/assets/templates/assets/system_user_detail.html +++ b/jumpserver/jumpserver/apps/assets/templates/assets/system_user_detail.html @@ -2,11 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} - {% block content %}
    @@ -17,11 +12,13 @@
  • {% trans 'Detail' %}
  • + {% if system_user.can_perm_to_asset %}
  • {% trans 'Assets' %}
  • + {% endif %}
  • {% trans 'Update' %}
  • @@ -144,6 +141,7 @@ {% endif %} + {% if system_user.is_need_test_asset_connective %} {% trans 'Test assets connective' %}: @@ -152,13 +150,14 @@ + {% endif %}
    - {% if system_user.protocol != 'rdp' %} + {% if system_user.is_need_cmd_filter %}
    diff --git a/jumpserver/jumpserver/apps/assets/templates/assets/system_user_list.html b/jumpserver/jumpserver/apps/assets/templates/assets/system_user_list.html index e6b259585ea07fac00448d0ec26363add18ffc56..0e88b64610285d82a6cbc382e020de728fde3864 100644 --- a/jumpserver/jumpserver/apps/assets/templates/assets/system_user_list.html +++ b/jumpserver/jumpserver/apps/assets/templates/assets/system_user_list.html @@ -2,36 +2,13 @@ {% load i18n %} {% block help_message %} -
    {% trans 'System user is Jumpserver jump login assets used by the users, can be understood as the user login assets, such as web, sa, the dba (` ssh web@some-host `), rather than using a user the username login server jump (` ssh xiaoming@some-host `); '%} {% trans 'In simple terms, users log into Jumpserver using their own username, and Jumpserver uses system users to log into assets. '%} {% trans 'When system users are created, if you choose auto push Jumpserver to use Ansible push system users into the asset, if the asset (Switch) does not support ansible, please manually fill in the account password.' %} -
    {% endblock %} {% block table_search %} - + {% include '_csv_import_export.html' %} {% endblock %} {% block table_container %} @@ -59,8 +36,6 @@ - {% include 'assets/_system_user_import_modal.html' %} - {% include 'assets/_system_user_update_modal.html' %} {% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/jumpserver/jumpserver/apps/assets/urls/api_urls.py b/jumpserver/jumpserver/apps/assets/urls/api_urls.py index 35651429bfbe39cb38bf9c47ff49be0b60d22123..653b714472ba7ea138e6dc831b0f2efbb5851000 100644 --- a/jumpserver/jumpserver/apps/assets/urls/api_urls.py +++ b/jumpserver/jumpserver/apps/assets/urls/api_urls.py @@ -12,6 +12,7 @@ app_name = 'assets' router = BulkRouter() router.register(r'assets', api.AssetViewSet, 'asset') +router.register(r'platforms', api.AssetPlatformViewSet, 'platform') router.register(r'admin-users', api.AdminUserViewSet, 'admin-user') router.register(r'system-users', api.SystemUserViewSet, 'system-user') router.register(r'labels', api.LabelViewSet, 'label') @@ -23,6 +24,8 @@ router.register(r'asset-users', api.AssetUserViewSet, 'asset-user') router.register(r'asset-users-info', api.AssetUserExportViewSet, 'asset-user-info') router.register(r'gathered-users', api.GatheredUserViewSet, 'gathered-user') router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset') +router.register(r'system-users-assets-relations', api.SystemUserAssetRelationViewSet, 'system-users-assets-relation') +router.register(r'system-users-nodes-relations', api.SystemUserNodeRelationViewSet, 'system-users-nodes-relation') cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filters', lookup='filter') cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule') @@ -35,6 +38,8 @@ urlpatterns = [ api.AssetAdminUserTestApi.as_view(), name='asset-alive-test'), path('assets//gateway/', api.AssetGatewayApi.as_view(), name='asset-gateway'), + path('assets//platform/', + api.AssetPlatformRetrieveApi.as_view(), name='asset-platform-detail'), path('asset-users/auth-info/', api.AssetUserAuthInfoApi.as_view(), name='asset-user-auth-info'), diff --git a/jumpserver/jumpserver/apps/assets/urls/views_urls.py b/jumpserver/jumpserver/apps/assets/urls/views_urls.py index 71483a4df9355cc6e0c914ee027fd45dc1d8e0a7..eec9dcc0dfed3ad5cede418eed3bb6e3fcd8f4d4 100644 --- a/jumpserver/jumpserver/apps/assets/urls/views_urls.py +++ b/jumpserver/jumpserver/apps/assets/urls/views_urls.py @@ -16,6 +16,11 @@ urlpatterns = [ # Asset user view path('asset//asset-user/', views.AssetUserListView.as_view(), name='asset-user-list'), + path('platform/', views.PlatformListView.as_view(), name='platform-list'), + path('platform/create/', views.PlatformCreateView.as_view(), name='platform-create'), + path('platform//', views.PlatformDetailView.as_view(), name='platform-detail'), + path('platform//update/', views.PlatformUpdateView.as_view(), name='platform-update'), + # User asset view path('user-asset/', views.UserAssetListView.as_view(), name='user-asset-list'), diff --git a/jumpserver/jumpserver/apps/assets/utils.py b/jumpserver/jumpserver/apps/assets/utils.py index e0b316ad7c20c73869da236ae102a7ae05f4d607..eaf3d502a4a6e9b0ed033ad44e74464e70c6c427 100644 --- a/jumpserver/jumpserver/apps/assets/utils.py +++ b/jumpserver/jumpserver/apps/assets/utils.py @@ -84,11 +84,15 @@ class TreeService(Tree): children_ids = self.all_children_ids(nid, with_self=with_self) return [self.get_node(i, deep=deep) for i in children_ids] - def ancestors(self, nid, with_self=False, deep=False): + def ancestors_ids(self, nid, with_self=True): ancestor_ids = list(self.rsearch(nid)) ancestor_ids.pop() if not with_self: ancestor_ids.pop(0) + return ancestor_ids + + def ancestors(self, nid, with_self=False, deep=False): + ancestor_ids = self.ancestors_ids(nid, with_self=with_self) return [self.get_node(i, deep=deep) for i in ancestor_ids] def get_node_full_tag(self, nid): diff --git a/jumpserver/jumpserver/apps/assets/views/__init__.py b/jumpserver/jumpserver/apps/assets/views/__init__.py index 04fc6c31ca9e5f6b0708972bb5e9c02a5ea7b275..74055a76cac2aee016a60c80e6e7441670a3a80b 100644 --- a/jumpserver/jumpserver/apps/assets/views/__init__.py +++ b/jumpserver/jumpserver/apps/assets/views/__init__.py @@ -1,5 +1,6 @@ # coding:utf-8 from .asset import * +from .platform import * from .system_user import * from .admin_user import * from .label import * diff --git a/jumpserver/jumpserver/apps/assets/views/asset.py b/jumpserver/jumpserver/apps/assets/views/asset.py index f08c08db857a70231cb9912ccf95a08fd54cb4c3..63179f4f8f2fb41feac7ae7edbdb201c66e0b5cd 100644 --- a/jumpserver/jumpserver/apps/assets/views/asset.py +++ b/jumpserver/jumpserver/apps/assets/views/asset.py @@ -74,7 +74,7 @@ class UserAssetListView(PermissionsMixin, TemplateView): class AssetCreateView(PermissionsMixin, FormMixin, TemplateView): model = Asset - form_class = forms.AssetCreateForm + form_class = forms.AssetCreateUpdateForm template_name = 'assets/asset_create.html' success_url = reverse_lazy('assets:asset-list') permission_classes = [IsOrgAdmin] @@ -110,7 +110,7 @@ class AssetCreateView(PermissionsMixin, FormMixin, TemplateView): class AssetUpdateView(PermissionsMixin, UpdateView): model = Asset - form_class = forms.AssetUpdateForm + form_class = forms.AssetCreateUpdateForm template_name = 'assets/asset_update.html' success_url = reverse_lazy('assets:asset-list') permission_classes = [IsOrgAdmin] diff --git a/jumpserver/jumpserver/apps/assets/views/domain.py b/jumpserver/jumpserver/apps/assets/views/domain.py index 7b4dcfcce16078d65f3f4fe76a0ce8101873922e..ad7fad1b6ab400745beddb4d8ff845c25e4fa329 100644 --- a/jumpserver/jumpserver/apps/assets/views/domain.py +++ b/jumpserver/jumpserver/apps/assets/views/domain.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- # -from django.views.generic import TemplateView, CreateView, \ - UpdateView, DeleteView, DetailView +from django.views.generic import ( + TemplateView, CreateView, UpdateView, DeleteView, DetailView +) from django.views.generic.detail import SingleObjectMixin from django.utils.translation import ugettext_lazy as _ from django.urls import reverse_lazy, reverse -from common.permissions import PermissionsMixin ,IsOrgAdmin +from common.permissions import PermissionsMixin, IsOrgAdmin from common.const import create_success_msg, update_success_msg from common.utils import get_object_or_none from ..models import Domain, Gateway diff --git a/jumpserver/jumpserver/apps/assets/views/platform.py b/jumpserver/jumpserver/apps/assets/views/platform.py new file mode 100644 index 0000000000000000000000000000000000000000..2e3aff49ccf2304b383f5cf10bcbd80cbc9c53cf --- /dev/null +++ b/jumpserver/jumpserver/apps/assets/views/platform.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +from django.views import generic +from django.utils.translation import ugettext as _ + +from common.permissions import PermissionsMixin, IsSuperUser +from ..models import Platform +from ..forms import PlatformForm, PlatformMetaForm + +__all__ = [ + 'PlatformListView', 'PlatformUpdateView', 'PlatformCreateView', + 'PlatformDetailView', +] + + +class PlatformListView(PermissionsMixin, generic.TemplateView): + template_name = 'assets/platform_list.html' + permission_classes = (IsSuperUser,) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context.update({ + 'app': _('Assets'), + 'action': _("Platform list"), + }) + return context + + +class PlatformCreateView(PermissionsMixin, generic.CreateView): + form_class = PlatformForm + permission_classes = (IsSuperUser,) + template_name = 'assets/platform_create_update.html' + model = Platform + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + meta_form = PlatformMetaForm() + context.update({ + 'app': _('Assets'), + 'action': _("Create platform"), + 'meta_form': meta_form, + }) + return context + + +class PlatformUpdateView(generic.UpdateView): + form_class = PlatformForm + permission_classes = (IsSuperUser,) + model = Platform + template_name = 'assets/platform_create_update.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + meta_form = PlatformMetaForm(initial=self.object.meta) + context.update({ + 'app': _('Assets'), + 'action': _("Update platform"), + 'type': 'update', + 'meta_form': meta_form, + }) + return context + + +class PlatformDetailView(generic.DetailView): + permission_classes = (IsSuperUser,) + model = Platform + template_name = 'assets/platform_detail.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context.update({ + 'app': _('Assets'), + 'action': _("Platform detail"), + }) + return context diff --git a/jumpserver/jumpserver/apps/audits/api.py b/jumpserver/jumpserver/apps/audits/api.py index 4d7165b4bf116b424afc9abe984b2bc06a0dce2e..3677a8e8e8dbedc409aaddd14495f81c5e1656c2 100644 --- a/jumpserver/jumpserver/apps/audits/api.py +++ b/jumpserver/jumpserver/apps/audits/api.py @@ -11,4 +11,4 @@ class FTPLogViewSet(OrgModelViewSet): model = FTPLog serializer_class = FTPLogSerializer permission_classes = (IsOrgAdminOrAppUser | IsOrgAuditor,) - + http_method_names = ['get', 'post', 'head', 'options'] diff --git a/jumpserver/jumpserver/apps/audits/migrations/0007_auto_20191202_1010.py b/jumpserver/jumpserver/apps/audits/migrations/0007_auto_20191202_1010.py new file mode 100644 index 0000000000000000000000000000000000000000..3c355ff69be79c4cab3303526c976a670db1d75e --- /dev/null +++ b/jumpserver/jumpserver/apps/audits/migrations/0007_auto_20191202_1010.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.7 on 2019-12-02 02:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('audits', '0006_auto_20190726_1753'), + ] + + operations = [ + migrations.AlterField( + model_name='ftplog', + name='remote_addr', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Remote addr'), + ), + migrations.AlterField( + model_name='operatelog', + name='remote_addr', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Remote addr'), + ), + migrations.AlterField( + model_name='passwordchangelog', + name='remote_addr', + field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Remote addr'), + ), + ] diff --git a/jumpserver/jumpserver/apps/audits/models.py b/jumpserver/jumpserver/apps/audits/models.py index 5b53d1c85f90b7e3603a4a025ecd0aafefc58fe1..81866bb1dae1ddcbe1b113895ec7287923e04bb6 100644 --- a/jumpserver/jumpserver/apps/audits/models.py +++ b/jumpserver/jumpserver/apps/audits/models.py @@ -16,7 +16,7 @@ __all__ = [ class FTPLog(OrgModelMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) user = models.CharField(max_length=128, verbose_name=_('User')) - remote_addr = models.CharField(max_length=15, verbose_name=_("Remote addr"), blank=True, null=True) + remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) asset = models.CharField(max_length=1024, verbose_name=_("Asset")) system_user = models.CharField(max_length=128, verbose_name=_("System user")) operate = models.CharField(max_length=16, verbose_name=_("Operate")) @@ -39,7 +39,7 @@ class OperateLog(OrgModelMixin): action = models.CharField(max_length=16, choices=ACTION_CHOICES, verbose_name=_("Action")) resource_type = models.CharField(max_length=64, verbose_name=_("Resource Type")) resource = models.CharField(max_length=128, verbose_name=_("Resource")) - remote_addr = models.CharField(max_length=15, verbose_name=_("Remote addr"), blank=True, null=True) + remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) datetime = models.DateTimeField(auto_now=True) def __str__(self): @@ -50,7 +50,7 @@ class PasswordChangeLog(models.Model): id = models.UUIDField(default=uuid.uuid4, primary_key=True) user = models.CharField(max_length=128, verbose_name=_('User')) change_by = models.CharField(max_length=128, verbose_name=_("Change by")) - remote_addr = models.CharField(max_length=15, verbose_name=_("Remote addr"), blank=True, null=True) + remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) datetime = models.DateTimeField(auto_now=True) def __str__(self): @@ -110,5 +110,14 @@ class UserLoginLog(models.Model): login_logs = login_logs.filter(username__in=username_list) return login_logs + @property + def reason_display(self): + from authentication.errors import reason_choices, old_reason_choices + reason = reason_choices.get(self.reason) + if reason: + return reason + reason = old_reason_choices.get(self.reason, self.reason) + return reason + class Meta: ordering = ['-datetime', 'username'] diff --git a/jumpserver/jumpserver/apps/audits/signals_handler.py b/jumpserver/jumpserver/apps/audits/signals_handler.py index 0f201b464a704fb614d387ed66f0e6329a1497a6..dab56fa5ca4d272aa9d48123cef5cf9897cb7ae2 100644 --- a/jumpserver/jumpserver/apps/audits/signals_handler.py +++ b/jumpserver/jumpserver/apps/audits/signals_handler.py @@ -4,18 +4,22 @@ from django.db.models.signals import post_save, post_delete from django.dispatch import receiver from django.db import transaction +from django.utils import timezone from rest_framework.renderers import JSONRenderer +from rest_framework.request import Request from jumpserver.utils import current_request from common.utils import get_request_ip, get_logger, get_syslogger from users.models import User +from users.signals import post_user_change_password +from authentication.signals import post_auth_failed, post_auth_success from terminal.models import Session, Command -from terminal.backends.command.serializers import SessionCommandSerializer +from common.utils.encode import model_to_json +from .utils import write_login_log from . import models -from . import serializers logger = get_logger(__name__) -sys_logger = get_syslogger("audits") +sys_logger = get_syslogger(__name__) json_render = JSONRenderer() @@ -23,6 +27,8 @@ MODELS_NEED_RECORD = ( 'User', 'UserGroup', 'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'Organization', 'AssetPermission', 'CommandFilter', 'CommandFilterRule', 'License', 'Setting', 'Account', 'SyncInstanceTask', + 'Platform', 'ChangeAuthPlan', 'GatherUserTask', + 'RemoteApp', 'RemoteAppPermission', 'DatabaseApp', 'DatabaseAppPermission', ) @@ -47,8 +53,11 @@ def create_operate_log(action, sender, resource): logger.error("Create operate log error: {}".format(e)) -@receiver(post_save, dispatch_uid="my_unique_identifier") -def on_object_created_or_update(sender, instance=None, created=False, **kwargs): +@receiver(post_save) +def on_object_created_or_update(sender, instance=None, created=False, update_fields=None, **kwargs): + if instance._meta.object_name == 'User' and \ + update_fields and 'last_login' in update_fields: + return if created: action = models.OperateLog.ACTION_CREATE else: @@ -56,46 +65,79 @@ def on_object_created_or_update(sender, instance=None, created=False, **kwargs): create_operate_log(action, sender, instance) -@receiver(post_delete, dispatch_uid="my_unique_identifier") +@receiver(post_delete) def on_object_delete(sender, instance=None, **kwargs): create_operate_log(models.OperateLog.ACTION_DELETE, sender, instance) -@receiver(post_save, sender=User, dispatch_uid="my_unique_identifier") -def on_user_change_password(sender, instance=None, **kwargs): - if hasattr(instance, '_set_password'): - if not current_request or not current_request.user.is_authenticated: - return - with transaction.atomic(): - models.PasswordChangeLog.objects.create( - user=instance, change_by=current_request.user, - remote_addr=get_request_ip(current_request), - ) +@receiver(post_user_change_password, sender=User) +def on_user_change_password(sender, user=None, **kwargs): + if not current_request: + remote_addr = '127.0.0.1' + change_by = 'System' + else: + remote_addr = get_request_ip(current_request) + if not current_request.user.is_authenticated: + change_by = str(user) + else: + change_by = str(current_request.user) + with transaction.atomic(): + models.PasswordChangeLog.objects.create( + user=str(user), change_by=change_by, + remote_addr=remote_addr, + ) def on_audits_log_create(sender, instance=None, **kwargs): if sender == models.UserLoginLog: category = "login_log" - serializer = serializers.LoginLogSerializer elif sender == models.FTPLog: - serializer = serializers.FTPLogSerializer category = "ftp_log" elif sender == models.OperateLog: category = "operation_log" - serializer = serializers.OperateLogSerializer elif sender == models.PasswordChangeLog: category = "password_change_log" - serializer = serializers.PasswordChangeLogSerializer elif sender == Session: category = "host_session_log" - serializer = serializers.SessionAuditSerializer elif sender == Command: category = "session_command_log" - serializer = SessionCommandSerializer else: return - s = serializer(instance=instance) - data = json_render.render(s.data).decode(errors='ignore') + data = model_to_json(instance, indent=None) msg = "{} - {}".format(category, data) sys_logger.info(msg) + + +def generate_data(username, request): + user_agent = request.META.get('HTTP_USER_AGENT', '') + login_ip = get_request_ip(request) or '0.0.0.0' + if isinstance(request, Request): + login_type = request.META.get('HTTP_X_JMS_LOGIN_TYPE', '') + else: + login_type = 'W' + + data = { + 'username': username, + 'ip': login_ip, + 'type': login_type, + 'user_agent': user_agent, + 'datetime': timezone.now() + } + return data + + +@receiver(post_auth_success) +def on_user_auth_success(sender, user, request, **kwargs): + logger.debug('User login success: {}'.format(user.username)) + data = generate_data(user.username, request) + data.update({'mfa': int(user.mfa_enabled), 'status': True}) + write_login_log(**data) + + +@receiver(post_auth_failed) +def on_user_auth_failed(sender, username, request, reason, **kwargs): + logger.debug('User login failed: {}'.format(username)) + data = generate_data(username, request) + data.update({'reason': reason, 'status': False}) + write_login_log(**data) diff --git a/jumpserver/jumpserver/apps/audits/tasks.py b/jumpserver/jumpserver/apps/audits/tasks.py index 90d8f47db1b0a5eb5b4f6619003389b5dd2a77ed..2dfe4a2dd3c80702f5e6ad4e2fe8194d81a86afd 100644 --- a/jumpserver/jumpserver/apps/audits/tasks.py +++ b/jumpserver/jumpserver/apps/audits/tasks.py @@ -6,7 +6,7 @@ from django.conf import settings from celery import shared_task from ops.celery.decorator import register_as_period_task -from .models import UserLoginLog +from .models import UserLoginLog, OperateLog @register_as_period_task(interval=3600*24) @@ -19,3 +19,15 @@ def clean_login_log_period(): days = 90 expired_day = now - datetime.timedelta(days=days) UserLoginLog.objects.filter(datetime__lt=expired_day).delete() + + +@register_as_period_task(interval=3600*24) +@shared_task +def clean_operation_log_period(): + now = timezone.now() + try: + days = int(settings.LOGIN_LOG_KEEP_DAYS) + except ValueError: + days = 90 + expired_day = now - datetime.timedelta(days=days) + OperateLog.objects.filter(datetime__lt=expired_day).delete() diff --git a/jumpserver/jumpserver/apps/audits/templates/audits/ftp_log_list.html b/jumpserver/jumpserver/apps/audits/templates/audits/ftp_log_list.html index 9e92f8481fb1467415c71910724cc1f7f1a53a18..e0399ca734c3d5fedb83e6990b39a5f5599974de 100644 --- a/jumpserver/jumpserver/apps/audits/templates/audits/ftp_log_list.html +++ b/jumpserver/jumpserver/apps/audits/templates/audits/ftp_log_list.html @@ -5,8 +5,6 @@ {% load common_tags %} {% block custom_head_css_js %} - - {% endblock %} diff --git a/jumpserver/jumpserver/apps/audits/templates/audits/login_log_list.html b/jumpserver/jumpserver/apps/audits/templates/audits/login_log_list.html index 151fccb13a1d912f18e49b65c8cca670a05778dd..1ac74d31127e74f1eaed2ec3db2130a8e02de919 100644 --- a/jumpserver/jumpserver/apps/audits/templates/audits/login_log_list.html +++ b/jumpserver/jumpserver/apps/audits/templates/audits/login_log_list.html @@ -78,7 +78,7 @@ {{ login_log.ip }} {{ login_log.city }} {{ login_log.get_mfa_display }} - {% trans login_log.reason %} + {{ login_log.reason_display }} {{ login_log.get_status_display }} {{ login_log.datetime }} @@ -102,47 +102,47 @@ {% block custom_foot_js %} - - + + }) + {% endblock %} diff --git a/jumpserver/jumpserver/apps/audits/templates/audits/operate_log_list.html b/jumpserver/jumpserver/apps/audits/templates/audits/operate_log_list.html index 31e219a85f8d4a0199322b4778b65def7319a992..fdc03ce359243496a785cecf2dbf93e1abe0dc70 100644 --- a/jumpserver/jumpserver/apps/audits/templates/audits/operate_log_list.html +++ b/jumpserver/jumpserver/apps/audits/templates/audits/operate_log_list.html @@ -5,8 +5,6 @@ {% load common_tags %} {% block custom_head_css_js %} - - @@ -69,30 +76,34 @@
    -
    +
    {% csrf_token %} + {% if form.non_field_errors %}
    - {% if block_login %} -

    {% trans 'Log in frequently and try again later' %}

    -

    {{ form.errors.password.as_text }}

    - {% elif password_expired %} -

    {% trans 'The user password has expired' %}

    - {% elif form.errors %} - {% if 'captcha' in form.errors %} -

    {% trans 'Captcha invalid' %}

    - {% else %} -

    {{ form.non_field_errors.as_text }}

    - {% endif %} -

    {{ form.errors.password.as_text }}

    - {% endif %} +

    {{ form.non_field_errors.as_text }}

    + {% elif form.errors.captcha %} +

    {% trans 'Captcha invalid' %}

    + {% else %} +
    + {% endif %}
    + {% if form.errors.username %} +
    +

    {{ form.errors.username.as_text }}

    +
    + {% endif %}
    + {% if form.errors.password %} +
    +

    {{ form.errors.password.as_text }}

    +
    + {% endif %}
    {{ form.captcha }} @@ -116,4 +127,4 @@
    - \ No newline at end of file + diff --git a/jumpserver/jumpserver/apps/authentication/urls/api_urls.py b/jumpserver/jumpserver/apps/authentication/urls/api_urls.py index 68dd8eeaabaee7ea0c241ecd0e15657ea1537d19..da59711c41249fec85e0f76b42660d98df3bc470 100644 --- a/jumpserver/jumpserver/apps/authentication/urls/api_urls.py +++ b/jumpserver/jumpserver/apps/authentication/urls/api_urls.py @@ -1,29 +1,25 @@ # coding:utf-8 # - -from __future__ import absolute_import - from django.urls import path from rest_framework.routers import DefaultRouter from .. import api +app_name = 'authentication' router = DefaultRouter() router.register('access-keys', api.AccessKeyViewSet, 'access-key') -app_name = 'authentication' - - urlpatterns = [ # path('token/', api.UserToken.as_view(), name='user-token'), - path('auth/', api.UserAuthApi.as_view(), name='user-auth'), + path('auth/', api.TokenCreateApi.as_view(), name='user-auth'), path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'), path('mfa/challenge/', api.MFAChallengeApi.as_view(), name='mfa-challenge'), path('connection-token/', api.UserConnectionTokenApi.as_view(), name='connection-token'), - path('otp/auth/', api.UserOtpAuthApi.as_view(), name='user-otp-auth'), path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'), + path('login-confirm-ticket/status/', api.TicketStatusApi.as_view(), name='login-confirm-ticket-status'), + path('login-confirm-settings//', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update') ] urlpatterns += router.urls diff --git a/jumpserver/jumpserver/apps/authentication/urls/view_urls.py b/jumpserver/jumpserver/apps/authentication/urls/view_urls.py index 8602daca5687b4475577697664d1d086233e2390..64d01ae346b93c116406abe68700195ab8bc8a5a 100644 --- a/jumpserver/jumpserver/apps/authentication/urls/view_urls.py +++ b/jumpserver/jumpserver/apps/authentication/urls/view_urls.py @@ -16,5 +16,7 @@ urlpatterns = [ # login path('login/', views.UserLoginView.as_view(), name='login'), path('login/otp/', views.UserLoginOtpView.as_view(), name='login-otp'), + path('login/wait-confirm/', views.UserLoginWaitConfirmView.as_view(), name='login-wait-confirm'), + path('login/guard/', views.UserLoginGuardView.as_view(), name='login-guard'), path('logout/', views.UserLogoutView.as_view(), name='logout'), ] diff --git a/jumpserver/jumpserver/apps/authentication/utils.py b/jumpserver/jumpserver/apps/authentication/utils.py index 70c7e52fad3ff7c6c7c64ed4af6d47a8352a3efb..197aa113ace4892b59c578d165018099d788b8ad 100644 --- a/jumpserver/jumpserver/apps/authentication/utils.py +++ b/jumpserver/jumpserver/apps/authentication/utils.py @@ -1,55 +1,25 @@ # -*- coding: utf-8 -*- # -from django.utils.translation import ugettext as _ from django.contrib.auth import authenticate -from common.utils import get_ip_city, get_object_or_none, validate_ip -from users.models import User -from . import const - - -def write_login_log(*args, **kwargs): - from audits.models import UserLoginLog - default_city = _("Unknown") - ip = kwargs.get('ip') or '' - if not (ip and validate_ip(ip)): - ip = ip[:15] - city = default_city - else: - city = get_ip_city(ip) or default_city - kwargs.update({'ip': ip, 'city': city}) - UserLoginLog.objects.create(**kwargs) +from . import errors def check_user_valid(**kwargs): password = kwargs.pop('password', None) public_key = kwargs.pop('public_key', None) - email = kwargs.pop('email', None) username = kwargs.pop('username', None) - - if username: - user = get_object_or_none(User, username=username) - elif email: - user = get_object_or_none(User, email=email) - else: - user = None - - if user is None: - return None, const.user_not_exist - elif not user.is_valid: - return None, const.user_invalid + request = kwargs.get('request') + + user = authenticate(request, username=username, + password=password, public_key=public_key) + if not user: + return None, errors.reason_password_failed + elif user.is_expired: + return None, errors.reason_user_inactive + elif not user.is_active: + return None, errors.reason_user_inactive elif user.password_has_expired: - return None, const.password_expired - - if password and authenticate(username=username, password=password): - return user, '' + return None, errors.reason_password_expired - if public_key and user.public_key: - public_key_saved = user.public_key.split() - if len(public_key_saved) == 1: - if public_key == public_key_saved[0]: - return user, '' - elif len(public_key_saved) > 1: - if public_key == public_key_saved[1]: - return user, '' - return None, const.password_failed + return user, '' diff --git a/jumpserver/jumpserver/apps/authentication/views/__init__.py b/jumpserver/jumpserver/apps/authentication/views/__init__.py index 5e7732adce0e807f54ab61cd1f347e106c2c7e75..5a1a40f7a492b1ec50385575970dfc3f58927757 100644 --- a/jumpserver/jumpserver/apps/authentication/views/__init__.py +++ b/jumpserver/jumpserver/apps/authentication/views/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- # - from .login import * +from .mfa import * diff --git a/jumpserver/jumpserver/apps/authentication/views/login.py b/jumpserver/jumpserver/apps/authentication/views/login.py index 2a7098ae71fd0eaae6541d1cbb2281bd168d544e..de3e0dd31bb413be120e02b9f705a045840f5dd8 100644 --- a/jumpserver/jumpserver/apps/authentication/views/login.py +++ b/jumpserver/jumpserver/apps/authentication/views/login.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import os +import datetime from django.core.cache import cache from django.contrib.auth import login as auth_login, logout as auth_logout from django.http import HttpResponse @@ -12,36 +13,32 @@ from django.utils.translation import ugettext as _ from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_protect from django.views.decorators.debug import sensitive_post_parameters -from django.views.generic.base import TemplateView +from django.views.generic.base import TemplateView, RedirectView from django.views.generic.edit import FormView from django.conf import settings +from django.urls import reverse_lazy -from common.utils import get_request_ip -from users.models import User -from audits.models import UserLoginLog as LoginLog +from common.utils import get_request_ip, get_object_or_none from users.utils import ( - check_otp_code, is_block_login, clean_failed_count, get_user_or_tmp_user, - set_tmp_user_to_cache, increase_login_failed_count, - redirect_user_first_login_or_index, + redirect_user_first_login_or_index, set_tmp_user_to_cache ) -from ..signals import post_auth_success, post_auth_failed -from .. import forms -from .. import const +from .. import forms, mixins, errors __all__ = [ - 'UserLoginView', 'UserLoginOtpView', 'UserLogoutView', + 'UserLoginView', 'UserLogoutView', + 'UserLoginGuardView', 'UserLoginWaitConfirmView', ] @method_decorator(sensitive_post_parameters(), name='dispatch') @method_decorator(csrf_protect, name='dispatch') @method_decorator(never_cache, name='dispatch') -class UserLoginView(FormView): +class UserLoginView(mixins.AuthMixin, FormView): form_class = forms.UserLoginForm form_class_captcha = forms.UserLoginCaptchaForm - redirect_field_name = 'next' key_prefix_captcha = "_LOGIN_INVALID_{}" + redirect_field_name = 'next' def get_template_names(self): template_name = 'authentication/login.html' @@ -52,7 +49,7 @@ class UserLoginView(FormView): if not License.has_valid_license(): return template_name - template_name = 'authentication/new_login.html' + template_name = 'authentication/xpack_login.html' return template_name def get(self, request, *args, **kwargs): @@ -63,53 +60,33 @@ class UserLoginView(FormView): # show jumpserver login page if request http://{JUMP-SERVER}/?admin=1 if settings.AUTH_OPENID and not self.request.GET.get('admin', 0): query_string = request.GET.urlencode() - login_url = "{}?{}".format(settings.LOGIN_URL, query_string) + openid_login_url = reverse_lazy("authentication:openid:openid-login") + login_url = "{}?{}".format(openid_login_url, query_string) return redirect(login_url) request.session.set_test_cookie() return super().get(request, *args, **kwargs) - def post(self, request, *args, **kwargs): - # limit login authentication - ip = get_request_ip(request) - username = self.request.POST.get('username') - if is_block_login(username, ip): - return self.render_to_response(self.get_context_data(block_login=True)) - return super().post(request, *args, **kwargs) - def form_valid(self, form): if not self.request.session.test_cookie_worked(): return HttpResponse(_("Please enable cookies and try again.")) - user = form.get_user() - # user password expired - if user.password_has_expired: - reason = const.password_expired - self.send_auth_signal(success=False, username=user.username, reason=reason) - return self.render_to_response(self.get_context_data(password_expired=True)) - - set_tmp_user_to_cache(self.request, user) - username = form.cleaned_data.get('username') - ip = get_request_ip(self.request) - # 登陆成功,清除缓存计数 - clean_failed_count(username, ip) - return redirect(self.get_success_url()) - - def form_invalid(self, form): - # write login failed log - username = form.cleaned_data.get('username') - exist = User.objects.filter(username=username).first() - reason = const.password_failed if exist else const.user_not_exist - # limit user login failed count - ip = get_request_ip(self.request) - increase_login_failed_count(username, ip) - form.add_limit_login_error(username, ip) - # show captcha - cache.set(self.key_prefix_captcha.format(ip), 1, 3600) - self.send_auth_signal(success=False, username=username, reason=reason) - - old_form = form - form = self.form_class_captcha(data=form.data) - form._errors = old_form.errors - return super().form_invalid(form) + try: + self.check_user_auth() + except errors.AuthFailedError as e: + form.add_error(None, e.msg) + ip = self.get_request_ip() + cache.set(self.key_prefix_captcha.format(ip), 1, 3600) + new_form = self.form_class_captcha(data=form.data) + new_form._errors = form.errors + context = self.get_context_data(form=new_form) + return self.render_to_response(context) + return self.redirect_to_guard_view() + + def redirect_to_guard_view(self): + guard_url = reverse('authentication:login-guard') + args = self.request.META.get('QUERY_STRING', '') + if args: + guard_url = "%s?%s" % (guard_url, args) + return redirect(guard_url) def get_form_class(self): ip = get_request_ip(self.request) @@ -118,21 +95,6 @@ class UserLoginView(FormView): else: return self.form_class - def get_success_url(self): - user = get_user_or_tmp_user(self.request) - - if user.otp_enabled and user.otp_secret_key: - # 1,2,mfa_setting & T - return reverse('authentication:login-otp') - elif user.otp_enabled and not user.otp_secret_key: - # 1,2,mfa_setting & F - return reverse('users:user-otp-enable-authentication') - elif not user.otp_enabled: - # 0 & T,F - auth_login(self.request, user) - self.send_auth_signal(success=True, user=user) - return redirect_user_first_login_or_index(self.request, self.redirect_field_name) - def get_context_data(self, **kwargs): context = { 'demo_mode': os.environ.get("DEMO_MODE"), @@ -141,51 +103,71 @@ class UserLoginView(FormView): kwargs.update(context) return super().get_context_data(**kwargs) - def send_auth_signal(self, success=True, user=None, username='', reason=''): - if success: - post_auth_success.send(sender=self.__class__, user=user, request=self.request) - else: - post_auth_failed.send( - sender=self.__class__, username=username, - request=self.request, reason=reason - ) - -class UserLoginOtpView(FormView): - template_name = 'authentication/login_otp.html' - form_class = forms.UserCheckOtpCodeForm +class UserLoginGuardView(mixins.AuthMixin, RedirectView): redirect_field_name = 'next' - - def form_valid(self, form): - user = get_user_or_tmp_user(self.request) - otp_code = form.cleaned_data.get('otp_code') - otp_secret_key = user.otp_secret_key - - if check_otp_code(otp_secret_key, otp_code): + login_url = reverse_lazy('authentication:login') + login_otp_url = reverse_lazy('authentication:login-otp') + login_confirm_url = reverse_lazy('authentication:login-wait-confirm') + + def format_redirect_url(self, url): + args = self.request.META.get('QUERY_STRING', '') + if args and self.query_string: + url = "%s?%s" % (url, args) + return url + + def get_redirect_url(self, *args, **kwargs): + try: + user = self.check_user_auth_if_need() + self.check_user_mfa_if_need(user) + self.check_user_login_confirm_if_need(user) + except errors.CredentialError: + return self.format_redirect_url(self.login_url) + except errors.MFARequiredError: + return self.format_redirect_url(self.login_otp_url) + except errors.LoginConfirmBaseError: + return self.format_redirect_url(self.login_confirm_url) + else: + # 启用但是没有设置otp, 排除radius + if user.mfa_enabled_but_not_set(): + # 1,2,mfa_setting & F + set_tmp_user_to_cache(self.request, user) + return reverse('users:user-otp-enable-authentication') auth_login(self.request, user) self.send_auth_signal(success=True, user=user) - return redirect(self.get_success_url()) - else: - self.send_auth_signal( - success=False, username=user.username, - reason=const.mfa_failed + self.clear_auth_mark() + url = redirect_user_first_login_or_index( + self.request, self.redirect_field_name ) - form.add_error( - 'otp_code', _('MFA code invalid, or ntp sync server time') - ) - return super().form_invalid(form) + return url + - def get_success_url(self): - return redirect_user_first_login_or_index(self.request, self.redirect_field_name) +class UserLoginWaitConfirmView(TemplateView): + template_name = 'authentication/login_wait_confirm.html' - def send_auth_signal(self, success=True, user=None, username='', reason=''): - if success: - post_auth_success.send(sender=self.__class__, user=user, request=self.request) + def get_context_data(self, **kwargs): + from tickets.models import Ticket + ticket_id = self.request.session.get("auth_ticket_id") + if not ticket_id: + ticket = None else: - post_auth_failed.send( - sender=self.__class__, username=username, - request=self.request, reason=reason - ) + ticket = get_object_or_none(Ticket, pk=ticket_id) + context = super().get_context_data(**kwargs) + if ticket: + timestamp_created = datetime.datetime.timestamp(ticket.date_created) + ticket_detail_url = reverse('tickets:ticket-detail', kwargs={'pk': ticket_id}) + msg = _("""Wait for {} confirm, You also can copy link to her/him
    + Don't close this page""").format(ticket.assignees_display) + else: + timestamp_created = 0 + ticket_detail_url = '' + msg = _("No ticket found") + context.update({ + "msg": msg, + "timestamp": timestamp_created, + "ticket_detail_url": ticket_detail_url + }) + return context @method_decorator(never_cache, name='dispatch') diff --git a/jumpserver/jumpserver/apps/authentication/views/mfa.py b/jumpserver/jumpserver/apps/authentication/views/mfa.py new file mode 100644 index 0000000000000000000000000000000000000000..57d6751daa65386ce9135635f760955b60f59781 --- /dev/null +++ b/jumpserver/jumpserver/apps/authentication/views/mfa.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# + +from __future__ import unicode_literals +from django.views.generic.edit import FormView +from .. import forms, errors, mixins +from .utils import redirect_to_guard_view + +__all__ = ['UserLoginOtpView'] + + +class UserLoginOtpView(mixins.AuthMixin, FormView): + template_name = 'authentication/login_otp.html' + form_class = forms.UserCheckOtpCodeForm + redirect_field_name = 'next' + + def form_valid(self, form): + otp_code = form.cleaned_data.get('otp_code') + try: + self.check_user_mfa(otp_code) + return redirect_to_guard_view() + except errors.MFAFailedError as e: + form.add_error('otp_code', e.msg) + return super().form_invalid(form) + diff --git a/jumpserver/jumpserver/apps/authentication/views/utils.py b/jumpserver/jumpserver/apps/authentication/views/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..182d7390b30c22abac1d1c641bc5f0ed12b710d1 --- /dev/null +++ b/jumpserver/jumpserver/apps/authentication/views/utils.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# +from django.shortcuts import reverse, redirect + + +def redirect_to_guard_view(): + continue_url = reverse('authentication:login-guard') + return redirect(continue_url) diff --git a/jumpserver/jumpserver/apps/common/drf/__init__.py b/jumpserver/jumpserver/apps/common/drf/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ec51c5a2b9dd623073fa752065a5b5a615780c7f --- /dev/null +++ b/jumpserver/jumpserver/apps/common/drf/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# diff --git a/jumpserver/jumpserver/apps/common/filters.py b/jumpserver/jumpserver/apps/common/drf/filters.py similarity index 99% rename from jumpserver/jumpserver/apps/common/filters.py rename to jumpserver/jumpserver/apps/common/drf/filters.py index 81138120b757601ad391ba002e20577605f967bd..c11a7864d2bae8d99ab8c37a13a1fca35d4c6e9a 100644 --- a/jumpserver/jumpserver/apps/common/filters.py +++ b/jumpserver/jumpserver/apps/common/drf/filters.py @@ -7,7 +7,7 @@ from rest_framework.serializers import ValidationError from django.core.cache import cache import logging -from . import const +from common import const __all__ = ["DatetimeRangeFilter", "IDSpmFilter", "CustomFilter"] diff --git a/jumpserver/jumpserver/apps/common/drfmetadata.py b/jumpserver/jumpserver/apps/common/drf/metadata.py similarity index 100% rename from jumpserver/jumpserver/apps/common/drfmetadata.py rename to jumpserver/jumpserver/apps/common/drf/metadata.py diff --git a/jumpserver/jumpserver/apps/common/parsers/__init__.py b/jumpserver/jumpserver/apps/common/drf/parsers/__init__.py similarity index 100% rename from jumpserver/jumpserver/apps/common/parsers/__init__.py rename to jumpserver/jumpserver/apps/common/drf/parsers/__init__.py diff --git a/jumpserver/jumpserver/apps/common/parsers/csv.py b/jumpserver/jumpserver/apps/common/drf/parsers/csv.py similarity index 98% rename from jumpserver/jumpserver/apps/common/parsers/csv.py rename to jumpserver/jumpserver/apps/common/drf/parsers/csv.py index 7cd7e1648c918fa99dfec619abd91dca3f1c2fc1..254c7337506cd4502079a6ba2321b4ef1cd21527 100644 --- a/jumpserver/jumpserver/apps/common/parsers/csv.py +++ b/jumpserver/jumpserver/apps/common/drf/parsers/csv.py @@ -9,7 +9,7 @@ import unicodecsv from rest_framework.parsers import BaseParser from rest_framework.exceptions import ParseError -from ..utils import get_logger +from common.utils import get_logger logger = get_logger(__file__) diff --git a/jumpserver/jumpserver/apps/common/renders/__init__.py b/jumpserver/jumpserver/apps/common/drf/renders/__init__.py similarity index 100% rename from jumpserver/jumpserver/apps/common/renders/__init__.py rename to jumpserver/jumpserver/apps/common/drf/renders/__init__.py diff --git a/jumpserver/jumpserver/apps/common/renders/csv.py b/jumpserver/jumpserver/apps/common/drf/renders/csv.py similarity index 98% rename from jumpserver/jumpserver/apps/common/renders/csv.py rename to jumpserver/jumpserver/apps/common/drf/renders/csv.py index 7eaac2b470a2745c71bd6584ce01dcf014099f87..d4ae9e6b8d44f150c544d427284512a227e3f232 100644 --- a/jumpserver/jumpserver/apps/common/renders/csv.py +++ b/jumpserver/jumpserver/apps/common/drf/renders/csv.py @@ -9,7 +9,7 @@ from six import BytesIO from rest_framework.renderers import BaseRenderer from rest_framework.utils import encoders, json -from ..utils import get_logger +from common.utils import get_logger logger = get_logger(__file__) diff --git a/jumpserver/jumpserver/apps/common/drf/routers.py b/jumpserver/jumpserver/apps/common/drf/routers.py new file mode 100644 index 0000000000000000000000000000000000000000..053a0590e829a651eda04f11df6444b30bee5f35 --- /dev/null +++ b/jumpserver/jumpserver/apps/common/drf/routers.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# +from rest_framework_nested.routers import NestedMixin +from rest_framework_bulk.routes import BulkRouter + +__all__ = ['BulkNestDefaultRouter'] + + +class BulkNestDefaultRouter(NestedMixin, BulkRouter): + pass diff --git a/jumpserver/jumpserver/apps/common/fields/form.py b/jumpserver/jumpserver/apps/common/fields/form.py index 156e331ddd9b3dc3480275d551f6a4e138bd9e75..c4cdc78ad283a09c6b9e26aadaa746fa3e875c83 100644 --- a/jumpserver/jumpserver/apps/common/fields/form.py +++ b/jumpserver/jumpserver/apps/common/fields/form.py @@ -6,9 +6,8 @@ from django import forms from django.utils import six from django.core.exceptions import ValidationError from django.utils.translation import ugettext as _ -from ..utils import get_signer +from ..utils import signer -signer = get_signer() __all__ = [ 'FormDictField', 'FormEncryptCharField', 'FormEncryptDictField', diff --git a/jumpserver/jumpserver/apps/common/fields/model.py b/jumpserver/jumpserver/apps/common/fields/model.py index e1bd4e1e7a767e6464aa8f84ef18526fce859cd2..ddb61f7c01d8b0c8e22ad2fcc0b797fbc9349a3c 100644 --- a/jumpserver/jumpserver/apps/common/fields/model.py +++ b/jumpserver/jumpserver/apps/common/fields/model.py @@ -4,7 +4,7 @@ import json from django.db import models from django.utils.translation import ugettext_lazy as _ -from ..utils import get_signer +from ..utils import signer __all__ = [ @@ -12,8 +12,8 @@ __all__ = [ 'JsonCharField', 'JsonTextField', 'JsonListCharField', 'JsonListTextField', 'JsonDictCharField', 'JsonDictTextField', 'EncryptCharField', 'EncryptTextField', 'EncryptMixin', 'EncryptJsonDictTextField', + 'EncryptJsonDictCharField', ] -signer = get_signer() class JsonMixin: @@ -108,14 +108,24 @@ class JsonTextField(JsonMixin, models.TextField): class EncryptMixin: + """ + EncryptMixin要放在最前面 + """ def from_db_value(self, value, expression, connection, context): - if value is not None: - return signer.unsign(value) - return None + if value is None: + return value + value = signer.unsign(value) + sp = super() + if hasattr(sp, 'from_db_value'): + return sp.from_db_value(value, expression, connection, context) + return value def get_prep_value(self, value): if value is None: return value + sp = super() + if hasattr(sp, 'get_prep_value'): + value = sp.get_prep_value(value) return signer.sign(value) @@ -150,3 +160,6 @@ class EncryptJsonDictTextField(EncryptMixin, JsonDictTextField): pass +class EncryptJsonDictCharField(EncryptMixin, JsonDictCharField): + pass + diff --git a/jumpserver/jumpserver/apps/common/fields/serializer.py b/jumpserver/jumpserver/apps/common/fields/serializer.py index 339be075c6eab323e464af9ff49e0bf600cc86f8..a05b493761f4bcc61ac9e49185631d44a39aea01 100644 --- a/jumpserver/jumpserver/apps/common/fields/serializer.py +++ b/jumpserver/jumpserver/apps/common/fields/serializer.py @@ -5,7 +5,10 @@ from rest_framework import serializers from django.utils import six -__all__ = ['StringIDField', 'StringManyToManyField', 'ChoiceDisplayField'] +__all__ = [ + 'StringIDField', 'StringManyToManyField', 'ChoiceDisplayField', + 'CustomMetaDictField' +] class StringIDField(serializers.Field): @@ -39,3 +42,70 @@ class DictField(serializers.DictField): if not value or not isinstance(value, dict): value = {} return super().to_representation(value) + + +class CustomMetaDictField(serializers.DictField): + """ + In use: + RemoteApp params field + CommandStorage meta field + ReplayStorage meta field + """ + type_fields_map = {} + default_type = None + convert_key_remove_type_prefix = False + convert_key_to_upper = False + + def filter_attribute(self, attribute, instance): + fields = self.type_fields_map.get(instance.type, []) + for field in fields: + if field.get('write_only', False): + attribute.pop(field['name'], None) + return attribute + + def get_attribute(self, instance): + """ + 序列化时调用 + """ + attribute = super().get_attribute(instance) + attribute = self.filter_attribute(attribute, instance) + return attribute + + def convert_value_key_remove_type_prefix(self, dictionary, value): + if not self.convert_key_remove_type_prefix: + return value + tp = dictionary.get('type') + prefix = '{}_'.format(tp) + convert_value = {} + for k, v in value.items(): + if k.lower().startswith(prefix): + k = k.lower().split(prefix, 1)[1] + convert_value[k] = v + return convert_value + + def convert_value_key_to_upper(self, value): + if not self.convert_key_to_upper: + return value + convert_value = {k.upper(): v for k, v in value.items()} + return convert_value + + def convert_value_key(self, dictionary, value): + value = self.convert_value_key_remove_type_prefix(dictionary, value) + value = self.convert_value_key_to_upper(value) + return value + + def filter_value_key(self, dictionary, value): + tp = dictionary.get('type') + fields = self.type_fields_map.get(tp, []) + fields_names = [field['name'] for field in fields] + filter_value = {k: v for k, v in value.items() if k in fields_names} + return filter_value + + def get_value(self, dictionary): + """ + 反序列化时调用 + """ + value = super().get_value(dictionary) + value = self.convert_value_key(dictionary, value) + value = self.filter_value_key(dictionary, value) + return value diff --git a/jumpserver/jumpserver/apps/common/mixins/api.py b/jumpserver/jumpserver/apps/common/mixins/api.py index 6b1e8a893f69ad70101d4ee761be0a5e24cf175c..694c5606d262c7a04d959b017b02294a05c01864 100644 --- a/jumpserver/jumpserver/apps/common/mixins/api.py +++ b/jumpserver/jumpserver/apps/common/mixins/api.py @@ -3,11 +3,11 @@ from django.http import JsonResponse from rest_framework.settings import api_settings -from ..filters import IDSpmFilter, CustomFilter +from common.drf.filters import IDSpmFilter, CustomFilter __all__ = [ "JSONResponseMixin", "CommonApiMixin", - "IDSpmFilterMixin", "CommonApiMixin", + "IDSpmFilterMixin", ] @@ -25,6 +25,22 @@ class IDSpmFilterMixin: return backends +class SerializerMixin: + def get_serializer_class(self): + serializer_class = None + if hasattr(self, 'serializer_classes') and \ + isinstance(self.serializer_classes, dict): + if self.action == 'list' and self.request.query_params.get('draw'): + serializer_class = self.serializer_classes.get('display') + if serializer_class is None: + serializer_class = self.serializer_classes.get( + self.action, self.serializer_classes.get('default') + ) + if serializer_class: + return serializer_class + return super().get_serializer_class() + + class ExtraFilterFieldsMixin: default_added_filters = [CustomFilter, IDSpmFilter] filter_backends = api_settings.DEFAULT_FILTER_BACKENDS @@ -44,5 +60,5 @@ class ExtraFilterFieldsMixin: return queryset -class CommonApiMixin(ExtraFilterFieldsMixin): +class CommonApiMixin(SerializerMixin, ExtraFilterFieldsMixin): pass diff --git a/jumpserver/jumpserver/apps/common/mixins/models.py b/jumpserver/jumpserver/apps/common/mixins/models.py index df3d899e0f89c13ab85c542e37fa5ef213db2eb6..e373a6b0894cf4d0ea84c19199a793fe70ae72aa 100644 --- a/jumpserver/jumpserver/apps/common/mixins/models.py +++ b/jumpserver/jumpserver/apps/common/mixins/models.py @@ -53,3 +53,15 @@ class CommonModelMixin(models.Model): class Meta: abstract = True + + +class DebugQueryManager(models.Manager): + def get_queryset(self): + import traceback + lines = traceback.format_stack() + print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>") + for line in lines[-10:-1]: + print(line) + print("<<<<<<<<<<<<<<<<<<<<<<<<<<<<") + queryset = super().get_queryset() + return queryset diff --git a/jumpserver/jumpserver/apps/common/permissions.py b/jumpserver/jumpserver/apps/common/permissions.py index 12bc5c6d4bb4078c74c9bd197b9ac65518da4fd6..43aa3fe3fb8219a2221fcdca38fbd2be7a9bc469 100644 --- a/jumpserver/jumpserver/apps/common/permissions.py +++ b/jumpserver/jumpserver/apps/common/permissions.py @@ -149,11 +149,7 @@ class CanUpdateDeleteUser(permissions.BasePermission): return False if obj.is_super_auditor: return False - if obj.is_org_admin: - return False - if len(obj.audit_orgs) > 1: - return False - if len(obj.user_orgs) > 1: + if obj.can_admin_current_org: return False return True @@ -174,12 +170,6 @@ class CanUpdateDeleteUser(permissions.BasePermission): return False if obj.is_super_auditor: return False - if obj.is_org_admin: - return False - if len(obj.audit_orgs) > 1: - return False - if len(obj.user_orgs) > 1: - return False return True def has_object_permission(self, request, view, obj): diff --git a/jumpserver/jumpserver/apps/common/signals_handlers.py b/jumpserver/jumpserver/apps/common/signals_handlers.py index 1fbfd536f67e794d0600f0373ca59924028a599f..d1cf1c7faea83c8e555fda3638a427640f7d705e 100644 --- a/jumpserver/jumpserver/apps/common/signals_handlers.py +++ b/jumpserver/jumpserver/apps/common/signals_handlers.py @@ -2,18 +2,22 @@ # import re import os +import logging from collections import defaultdict from django.conf import settings from django.dispatch import receiver from django.core.signals import request_finished from django.db import connection +from django.conf import LazySettings +from django.db.utils import ProgrammingError, OperationalError +from jumpserver.utils import get_current_request -from common.utils import get_logger from .local import thread_local +from .signals import django_ready pattern = re.compile(r'FROM `(\w+)`') -logger = get_logger(__name__) +logger = logging.getLogger("jumpserver.common") DEBUG_DB = os.environ.get('DEBUG_DB', '0') == '1' @@ -47,22 +51,40 @@ def on_request_finished_logging_db_query(sender, **kwargs): counters['total'].time += float(time) counters = sorted(counters.items(), key=lambda x: x[1]) + if not counters: + return + method = 'GET' + path = '/Unknown' + current_request = get_current_request() + if current_request: + method = current_request.method + path = current_request.get_full_path() + logger.debug(">>> [{}] {}".format(method, path)) for name, counter in counters: logger.debug("Query {:3} times using {:.2f}s {}".format( counter.counter, counter.time, name) ) -@receiver(request_finished) def on_request_finished_release_local(sender, **kwargs): thread_local.__release_local__() if settings.DEBUG and DEBUG_DB: request_finished.connect(on_request_finished_logging_db_query) - - - - - - +else: + request_finished.connect(on_request_finished_release_local) + + +@receiver(django_ready) +def monkey_patch_settings(sender, **kwargs): + def monkey_patch_getattr(self, name): + val = getattr(self._wrapped, name) + if callable(val): + val = val() + return val + + try: + LazySettings.__getattr__ = monkey_patch_getattr + except (ProgrammingError, OperationalError): + pass diff --git a/jumpserver/jumpserver/apps/common/tasks.py b/jumpserver/jumpserver/apps/common/tasks.py index 912ef28c3248dccbebdd3766df2bec7ee0d4b666..aecc54ca77fd39bef74860d410eb438a74282771 100644 --- a/jumpserver/jumpserver/apps/common/tasks.py +++ b/jumpserver/jumpserver/apps/common/tasks.py @@ -1,6 +1,7 @@ from django.core.mail import send_mail from django.conf import settings from celery import shared_task + from .utils import get_logger diff --git a/jumpserver/jumpserver/apps/common/tests.py b/jumpserver/jumpserver/apps/common/tests.py index a9edb8f693c8ac3db6ad89f97e8b7b61d03bdcfc..5fd1a7ddb50237a44928147c121b113f24fd649e 100644 --- a/jumpserver/jumpserver/apps/common/tests.py +++ b/jumpserver/jumpserver/apps/common/tests.py @@ -2,11 +2,10 @@ from django.test import TestCase # Create your tests here. -from .utils import random_string, get_signer +from .utils import random_string, signer def test_signer_len(): - signer = get_signer() results = {} for i in range(1, 4096): s = random_string(i) diff --git a/jumpserver/jumpserver/apps/common/tree.py b/jumpserver/jumpserver/apps/common/tree.py index 1ffe4347efc223abf6106a3170371508fefc65eb..fbade582e9f4c045b4b56dae04bbb97096c254da 100644 --- a/jumpserver/jumpserver/apps/common/tree.py +++ b/jumpserver/jumpserver/apps/common/tree.py @@ -98,4 +98,5 @@ class TreeNodeSerializer(serializers.Serializer): isParent = serializers.BooleanField(default=False) open = serializers.BooleanField(default=False) iconSkin = serializers.CharField(max_length=128, allow_blank=True) + nocheck = serializers.BooleanField(default=False) meta = serializers.JSONField() diff --git a/jumpserver/jumpserver/apps/common/utils/common.py b/jumpserver/jumpserver/apps/common/utils/common.py index 2f4ce784c9507e7373b7fdafbcf56736c84ef2af..07116ea8b06561552d4acab76468c6171b213c36 100644 --- a/jumpserver/jumpserver/apps/common/utils/common.py +++ b/jumpserver/jumpserver/apps/common/utils/common.py @@ -9,6 +9,7 @@ import uuid from functools import wraps import time import ipaddress +import psutil UUID_PATTERN = re.compile(r'\w{8}(-\w{4}){3}-\w{12}') @@ -26,12 +27,12 @@ def combine_seq(s1, s2, callback=None): return seq -def get_logger(name=None): +def get_logger(name=''): return logging.getLogger('jumpserver.%s' % name) -def get_syslogger(name=None): - return logging.getLogger('jms.%s' % name) +def get_syslogger(name=''): + return logging.getLogger('syslog.%s' % name) def timesince(dt, since='', default="just now"): @@ -153,6 +154,14 @@ def get_request_ip(request): return login_ip +def get_request_ip_or_data(request): + ip = '' + if hasattr(request, 'data'): + ip = request.data.get('remote_addr', '') + ip = ip or get_request_ip(request) + return ip + + def validate_ip(ip): try: ipaddress.ip_address(ip) @@ -226,3 +235,10 @@ class lazyproperty: value = self.func(instance) setattr(instance, self.func.__name__, value) return value + + +def get_disk_usage(): + partitions = psutil.disk_partitions() + mount_points = [p.mountpoint for p in partitions] + usages = {p: psutil.disk_usage(p) for p in mount_points} + return usages diff --git a/jumpserver/jumpserver/apps/common/utils/django.py b/jumpserver/jumpserver/apps/common/utils/django.py index 9a0af8f7dab0dd554433259b7b52d9cb384d6852..815bb16b1e33f1d1e574365e3cdb8108053a4fd0 100644 --- a/jumpserver/jumpserver/apps/common/utils/django.py +++ b/jumpserver/jumpserver/apps/common/utils/django.py @@ -35,20 +35,3 @@ def date_expired_default(): years = 70 return timezone.now() + timezone.timedelta(days=365*years) - -def get_command_storage_setting(): - default = settings.DEFAULT_TERMINAL_COMMAND_STORAGE - value = settings.TERMINAL_COMMAND_STORAGE - if not value: - return default - value.update(default) - return value - - -def get_replay_storage_setting(): - default = settings.DEFAULT_TERMINAL_REPLAY_STORAGE - value = settings.TERMINAL_REPLAY_STORAGE - if not value: - return default - value.update(default) - return value diff --git a/jumpserver/jumpserver/apps/common/utils/encode.py b/jumpserver/jumpserver/apps/common/utils/encode.py index d4e9c4cbee4fb8104a4b46dcbfb6a68cd52f718d..097e282925cc7d0833d2b43b9be0b9c6f48a56dd 100644 --- a/jumpserver/jumpserver/apps/common/utils/encode.py +++ b/jumpserver/jumpserver/apps/common/utils/encode.py @@ -1,12 +1,14 @@ # -*- coding: utf-8 -*- # import re +import json from six import string_types import base64 import os import time import hashlib from io import StringIO +from itertools import chain import paramiko import sshpubkeys @@ -15,6 +17,8 @@ from itsdangerous import ( BadSignature, SignatureExpired ) from django.conf import settings +from django.core.serializers.json import DjangoJSONEncoder +from django.db.models.fields.files import FileField from .http import http_date @@ -180,9 +184,44 @@ def encrypt_password(password, salt=None): def get_signer(): - signer = Signer(settings.SECRET_KEY) - return signer + s = Signer(settings.SECRET_KEY) + return s + + +signer = get_signer() def ensure_last_char_is_ascii(data): remain = '' + + +secret_pattern = re.compile(r'password|secret|key', re.IGNORECASE) + + +def model_to_dict_pro(instance, fields=None, exclude=None): + from ..fields.model import EncryptMixin + opts = instance._meta + data = {} + for f in chain(opts.concrete_fields, opts.private_fields): + if not getattr(f, 'editable', False): + continue + if fields and f.name not in fields: + continue + if exclude and f.name in exclude: + continue + if isinstance(f, FileField): + continue + if isinstance(f, EncryptMixin): + continue + if secret_pattern.search(f.name): + continue + value = f.value_from_object(instance) + data[f.name] = value + return data + + +def model_to_json(instance, sort_keys=True, indent=2, cls=None): + data = model_to_dict_pro(instance) + if cls is None: + cls = DjangoJSONEncoder + return json.dumps(data, sort_keys=sort_keys, indent=indent, cls=cls) diff --git a/jumpserver/jumpserver/apps/common/utils/ipip/__init__.py b/jumpserver/jumpserver/apps/common/utils/ipip/__init__.py index 864ac8d4424d7742eb21919793ed336f35a6bda2..103b3665438fb1f83bb09550bf707e37b8442b77 100644 --- a/jumpserver/jumpserver/apps/common/utils/ipip/__init__.py +++ b/jumpserver/jumpserver/apps/common/utils/ipip/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- # -from .ipdb import * +from .utils import * diff --git a/jumpserver/jumpserver/apps/common/utils/ipip/ipdb.py b/jumpserver/jumpserver/apps/common/utils/ipip/utils.py similarity index 61% rename from jumpserver/jumpserver/apps/common/utils/ipip/ipdb.py rename to jumpserver/jumpserver/apps/common/utils/ipip/utils.py index e17fb523ce60ab8f01aabd24ec1b1e09e7d25874..9dd571a8f0e142542cffe28133de46b07feab1a3 100644 --- a/jumpserver/jumpserver/apps/common/utils/ipip/ipdb.py +++ b/jumpserver/jumpserver/apps/common/utils/ipip/utils.py @@ -1,18 +1,24 @@ # -*- coding: utf-8 -*- # import os +from django.utils.translation import ugettext as _ import ipdb +__all__ = ['get_ip_city'] ipip_db = None def get_ip_city(ip): global ipip_db + if not ip or not isinstance(ip, str): + return _("Invalid ip") + if ':' in ip: + return 'IPv6' if ipip_db is None: ipip_db_path = os.path.join(os.path.dirname(__file__), 'ipipfree.ipdb') ipip_db = ipdb.City(ipip_db_path) info = list(set(ipip_db.find(ip, 'CN'))) if '' in info: info.remove('') - return ' '.join(info) \ No newline at end of file + return ' '.join(info) diff --git a/jumpserver/jumpserver/apps/jumpserver/conf.py b/jumpserver/jumpserver/apps/jumpserver/conf.py index f7f35c2150e02ba0dc67cb51779a3fc8851ac30b..70876623eab406f0c11ebdd1bf47076194970e0b 100644 --- a/jumpserver/jumpserver/apps/jumpserver/conf.py +++ b/jumpserver/jumpserver/apps/jumpserver/conf.py @@ -1,6 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # +""" +配置分类: +1. Django使用的配置文件,写到settings中 +2. 程序需要, 用户不需要更改的写到settings中 +3. 程序需要, 用户需要更改的写到本config中 +""" import os import sys import types @@ -8,6 +14,7 @@ import errno import json import yaml from importlib import import_module +from django.urls import reverse_lazy BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_DIR = os.path.dirname(BASE_DIR) @@ -29,6 +36,10 @@ def import_string(dotted_path): ) from err +class DoesNotExist(Exception): + pass + + 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 @@ -72,34 +83,234 @@ class Config(dict): the application's :attr:`~flask.Flask.root_path`. :param defaults: an optional dictionary of default values """ + defaults = { + # Django Config + 'SECRET_KEY': '', + 'BOOTSTRAP_TOKEN': '', + 'DEBUG': True, + 'SITE_URL': 'http://localhost:8080', + 'LOG_LEVEL': 'DEBUG', + 'LOG_DIR': os.path.join(PROJECT_DIR, 'logs'), + 'DB_ENGINE': 'mysql', + 'DB_NAME': 'jumpserver', + 'DB_HOST': '127.0.0.1', + 'DB_PORT': 3306, + 'DB_USER': 'root', + 'DB_PASSWORD': '', + 'REDIS_HOST': '127.0.0.1', + 'REDIS_PORT': 6379, + 'REDIS_PASSWORD': '', + 'REDIS_DB_CELERY': 3, + 'REDIS_DB_CACHE': 4, + 'REDIS_DB_SESSION': 5, + 'REDIS_DB_WS': 6, + 'CAPTCHA_TEST_MODE': None, + 'TOKEN_EXPIRATION': 3600 * 24, + 'DISPLAY_PER_PAGE': 25, + 'DEFAULT_EXPIRED_YEARS': 70, + 'SESSION_COOKIE_DOMAIN': None, + 'CSRF_COOKIE_DOMAIN': None, + 'SESSION_COOKIE_AGE': 3600 * 24, + 'SESSION_EXPIRE_AT_BROWSER_CLOSE': False, + 'LOGIN_URL': reverse_lazy('authentication:login'), + + # Custom Config + # Auth LDAP settings + 'AUTH_LDAP': False, + 'AUTH_LDAP_SERVER_URI': 'ldap://localhost:389', + 'AUTH_LDAP_BIND_DN': 'cn=admin,dc=jumpserver,dc=org', + 'AUTH_LDAP_BIND_PASSWORD': '', + 'AUTH_LDAP_SEARCH_OU': 'ou=tech,dc=jumpserver,dc=org', + 'AUTH_LDAP_SEARCH_FILTER': '(cn=%(user)s)', + 'AUTH_LDAP_START_TLS': False, + 'AUTH_LDAP_USER_ATTR_MAP': {"username": "cn", "name": "sn", "email": "mail"}, + 'AUTH_LDAP_CONNECT_TIMEOUT': 30, + 'AUTH_LDAP_SEARCH_PAGED_SIZE': 1000, + 'AUTH_LDAP_SYNC_IS_PERIODIC': False, + 'AUTH_LDAP_SYNC_INTERVAL': None, + 'AUTH_LDAP_SYNC_CRONTAB': None, + 'AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS': False, + 'AUTH_LDAP_OPTIONS_OPT_REFERRALS': -1, + + 'AUTH_OPENID': False, + 'BASE_SITE_URL': 'http://localhost:8080', + 'AUTH_OPENID_SERVER_URL': 'http://openid', + 'AUTH_OPENID_REALM_NAME': 'jumpserver', + 'AUTH_OPENID_CLIENT_ID': 'jumpserver', + 'AUTH_OPENID_CLIENT_SECRET': '', + 'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True, + 'AUTH_OPENID_SHARE_SESSION': True, + + 'AUTH_RADIUS': False, + 'RADIUS_SERVER': 'localhost', + 'RADIUS_PORT': 1812, + 'RADIUS_SECRET': '', + 'RADIUS_ENCRYPT_PASSWORD': True, + 'OTP_IN_RADIUS': False, + + 'OTP_VALID_WINDOW': 2, + 'OTP_ISSUER_NAME': 'Jumpserver', + 'EMAIL_SUFFIX': 'jumpserver.org', + + 'TERMINAL_PASSWORD_AUTH': True, + 'TERMINAL_PUBLIC_KEY_AUTH': True, + 'TERMINAL_HEARTBEAT_INTERVAL': 20, + 'TERMINAL_ASSET_LIST_SORT_BY': 'hostname', + 'TERMINAL_ASSET_LIST_PAGE_SIZE': 'auto', + 'TERMINAL_SESSION_KEEP_DURATION': 9999, + 'TERMINAL_HOST_KEY': '', + 'TERMINAL_TELNET_REGEX': '', + 'TERMINAL_COMMAND_STORAGE': {}, + + 'SECURITY_MFA_AUTH': False, + 'SECURITY_SERVICE_ACCOUNT_REGISTRATION': True, + 'SECURITY_VIEW_AUTH_NEED_MFA': True, + 'SECURITY_LOGIN_LIMIT_COUNT': 7, + 'SECURITY_LOGIN_LIMIT_TIME': 30, + 'SECURITY_MAX_IDLE_TIME': 30, + 'SECURITY_PASSWORD_EXPIRATION_TIME': 9999, + 'SECURITY_PASSWORD_MIN_LENGTH': 6, + 'SECURITY_PASSWORD_UPPER_CASE': False, + 'SECURITY_PASSWORD_LOWER_CASE': False, + 'SECURITY_PASSWORD_NUMBER': False, + 'SECURITY_PASSWORD_SPECIAL_CHAR': False, + + 'HTTP_BIND_HOST': '0.0.0.0', + 'HTTP_LISTEN_PORT': 8080, + 'WS_LISTEN_PORT': 8070, + 'LOGIN_LOG_KEEP_DAYS': 90, + 'ASSETS_PERM_CACHE_TIME': 3600 * 24, + 'SECURITY_MFA_VERIFY_TTL': 3600, + 'ASSETS_PERM_CACHE_ENABLE': False, + 'SYSLOG_ADDR': '', # '192.168.0.1:514' + 'SYSLOG_FACILITY': 'user', + 'SYSLOG_SOCKTYPE': 2, + 'PERM_SINGLE_ASSET_TO_UNGROUP_NODE': False, + 'WINDOWS_SSH_DEFAULT_SHELL': 'cmd', + 'FLOWER_URL': "127.0.0.1:5555", + 'DEFAULT_ORG_SHOW_ALL_USERS': True, + 'PERIOD_TASK_ENABLE': True, + 'FORCE_SCRIPT_NAME': '', + 'LOGIN_CONFIRM_ENABLE': False, + 'WINDOWS_SKIP_ALL_MANUAL_PASSWORD': False, + } - def __init__(self, root_path=None, defaults=None): - self.defaults = defaults or {} - self.root_path = root_path - super().__init__({}) + def convert_type(self, k, v): + default_value = self.defaults.get(k) + if default_value is None: + return v + tp = type(default_value) + # 对bool特殊处理 + if tp is bool and isinstance(v, str): + if v in ("true", "True", "1"): + return True + else: + return False + if tp in [list, dict] and isinstance(v, str): + try: + v = json.loads(v) + return v + except json.JSONDecodeError: + return v - def from_envvar(self, variable_name, silent=False): - """Loads a configuration from an environment variable pointing to - a configuration file. This is basically just a shortcut with nicer - error messages for this line of code:: + try: + if tp in [list, dict]: + v = json.loads(v) + else: + v = tp(v) + except Exception: + pass + return v - app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) + def __repr__(self): + return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self)) - :param variable_name: name of the environment variable - :param silent: set to ``True`` if you want silent failure for missing - files. - :return: bool. ``True`` if able to load config, ``False`` otherwise. - """ - rv = os.environ.get(variable_name) - if not rv: - if silent: - return False - raise RuntimeError('The environment variable %r is not set ' - 'and as such configuration could not be ' - 'loaded. Set this variable and make it ' - 'point to a configuration file' % - variable_name) - return self.from_pyfile(rv, silent=silent) + def get_from_config(self, item): + try: + value = super().__getitem__(item) + except KeyError: + value = None + return value + + def get_from_env(self, item): + value = os.environ.get(item, None) + if value is not None: + value = self.convert_type(item, value) + return value + + 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 + return self.defaults.get(item) + + def __getitem__(self, item): + return self.get(item) + + def __getattr__(self, item): + return self.get(item) + + +class DynamicConfig: + def __init__(self, static_config): + self.static_config = static_config + self.db_setting = None + + def __getitem__(self, item): + return self.dynamic(item) + + def __getattr__(self, item): + return self.dynamic(item) + + def dynamic(self, item): + return lambda: self.get(item) + + def LOGIN_URL(self): + auth_openid = self.get('AUTH_OPENID') + if auth_openid: + return reverse_lazy("authentication:openid:openid-login") + return self.get('LOGIN_URL') + + def AUTHENTICATION_BACKENDS(self): + backends = [ + 'authentication.backends.pubkey.PublicKeyAuthBackend', + 'django.contrib.auth.backends.ModelBackend', + ] + if self.get('AUTH_LDAP'): + backends.insert(0, 'authentication.backends.ldap.LDAPAuthorizationBackend') + if self.static_config.get('AUTH_OPENID'): + backends.insert(0, 'authentication.backends.openid.backends.OpenIDAuthorizationPasswordBackend') + backends.insert(0, 'authentication.backends.openid.backends.OpenIDAuthorizationCodeBackend') + if self.static_config.get('AUTH_RADIUS'): + backends.insert(0, 'authentication.backends.radius.RadiusBackend') + return backends + + def get_from_db(self, item): + if self.db_setting is not None: + value = self.db_setting.get(item) + if value is not None: + return value + return None + + def get(self, item): + # 先从数据库中获取 + value = self.get_from_db(item) + if value is not None: + return value + return self.static_config.get(item) + + +class ConfigManager: + config_class = Config + + def __init__(self, root_path=None): + self.root_path = root_path + self.config = self.config_class() def from_pyfile(self, filename, silent=False): """Updates the values in the config from a Python file. This function @@ -162,7 +373,7 @@ class Config(dict): obj = import_string(obj) for key in dir(obj): if key.isupper(): - self[key] = getattr(obj, key) + self.config[key] = getattr(obj, key) def from_json(self, filename, silent=False): """Updates the values in the config from a JSON file. This function @@ -224,214 +435,50 @@ class Config(dict): for mapping in mappings: for (key, value) in mapping: if key.isupper(): - self[key] = value + self.config[key] = value return True - def get_namespace(self, namespace, lowercase=True, trim_namespace=True): - """Returns a dictionary containing a subset of configuration options - that match the specified namespace/prefix. Example usage:: - - app.config['IMAGE_STORE_TYPE'] = 'fs' - app.config['IMAGE_STORE_PATH'] = '/var/app/images' - app.config['IMAGE_STORE_BASE_URL'] = 'http://img.website.com' - image_store_config = app.config.get_namespace('IMAGE_STORE_') - - The resulting dictionary `image_store_config` would look like:: - - { - 'types': 'fs', - 'path': '/var/app/images', - 'base_url': 'http://img.website.com' - } - - This is often useful when configuration options map directly to - keyword arguments in functions or class constructors. - - :param namespace: a configuration namespace - :param lowercase: a flag indicating if the keys of the resulting - dictionary should be lowercase - :param trim_namespace: a flag indicating if the keys of the resulting - dictionary should not include the namespace + def load_from_object(self): + sys.path.insert(0, PROJECT_DIR) + try: + from config import config as c + self.from_object(c) + return True + except ImportError: + pass + return False - .. versionadded:: 0.11 - """ - rv = {} - for k, v in self.items(): - if not k.startswith(namespace): + def load_from_yml(self): + for i in ['config.yml', 'config.yaml']: + if not os.path.isfile(os.path.join(self.root_path, i)): continue - if trim_namespace: - key = k[len(namespace):] - else: - key = k - if lowercase: - key = key.lower() - rv[key] = v - return rv - - def convert_type(self, k, v): - default_value = self.defaults.get(k) - if default_value is None: - return v - tp = type(default_value) - # 对bool特殊处理 - if tp is bool and isinstance(v, str): - if v in ("true", "True", "1"): + loaded = self.from_yaml(i) + if loaded: return True - else: - return False - if tp in [list, dict] and isinstance(v, str): - try: - v = json.loads(v) - return v - except json.JSONDecodeError: - return v + return False - try: - if tp in [list, dict]: - v = json.loads(v) - else: - v = tp(v) - except Exception: - pass - return v + @classmethod + def load_user_config(cls, root_path=None, config_class=None): + config_class = config_class or Config + cls.config_class = config_class + if not root_path: + root_path = PROJECT_DIR - def __repr__(self): - return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self)) + manager = cls(root_path=root_path) + if manager.load_from_object(): + return manager.config + elif manager.load_from_yml(): + return manager.config + else: + msg = """ - def __getitem__(self, item): - # 先从设置的来 - try: - value = super().__getitem__(item) - except KeyError: - value = None - if value is not None: - return value - # 其次从环境变量来 - value = os.environ.get(item, None) - if value is not None: - return self.convert_type(item, value) - return self.defaults.get(item) + Error: No config file found. - def __getattr__(self, item): - return self.__getitem__(item) - - -defaults = { - 'SECRET_KEY': '', - 'BOOTSTRAP_TOKEN': '', - 'DEBUG': True, - 'SITE_URL': 'http://localhost', - 'LOG_LEVEL': 'DEBUG', - 'LOG_DIR': os.path.join(PROJECT_DIR, 'logs'), - 'DB_ENGINE': 'mysql', - 'DB_NAME': 'jumpserver', - 'DB_HOST': '127.0.0.1', - 'DB_PORT': 3306, - 'DB_USER': 'root', - 'DB_PASSWORD': '', - 'REDIS_HOST': '127.0.0.1', - 'REDIS_PORT': 6379, - 'REDIS_PASSWORD': '', - 'REDIS_DB_CELERY': 3, - 'REDIS_DB_CACHE': 4, - 'REDIS_DB_SESSION': 5, - 'REDIS_DB_WS': 6, - 'CAPTCHA_TEST_MODE': None, - 'TOKEN_EXPIRATION': 3600 * 24, - 'DISPLAY_PER_PAGE': 25, - 'DEFAULT_EXPIRED_YEARS': 70, - 'SESSION_COOKIE_DOMAIN': None, - 'CSRF_COOKIE_DOMAIN': None, - 'SESSION_COOKIE_AGE': 3600 * 24, - 'SESSION_EXPIRE_AT_BROWSER_CLOSE': False, - 'AUTH_OPENID': False, - 'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True, - 'AUTH_OPENID_SHARE_SESSION': True, - 'OTP_VALID_WINDOW': 2, - 'OTP_ISSUER_NAME': 'Jumpserver', - 'EMAIL_SUFFIX': 'jumpserver.org', - 'TERMINAL_PASSWORD_AUTH': True, - 'TERMINAL_PUBLIC_KEY_AUTH': True, - 'TERMINAL_HEARTBEAT_INTERVAL': 20, - 'TERMINAL_ASSET_LIST_SORT_BY': 'hostname', - 'TERMINAL_ASSET_LIST_PAGE_SIZE': 'auto', - 'TERMINAL_SESSION_KEEP_DURATION': 9999, - 'TERMINAL_HOST_KEY': '', - 'TERMINAL_TELNET_REGEX': '', - 'TERMINAL_COMMAND_STORAGE': {}, - 'SECURITY_MFA_AUTH': False, - 'SECURITY_SERVICE_ACCOUNT_REGISTRATION': True, - 'SECURITY_VIEW_AUTH_NEED_MFA': True, - 'SECURITY_LOGIN_LIMIT_COUNT': 7, - 'SECURITY_LOGIN_LIMIT_TIME': 30, - 'SECURITY_MAX_IDLE_TIME': 30, - 'SECURITY_PASSWORD_EXPIRATION_TIME': 9999, - 'SECURITY_PASSWORD_MIN_LENGTH': 6, - 'SECURITY_PASSWORD_UPPER_CASE': False, - 'SECURITY_PASSWORD_LOWER_CASE': False, - 'SECURITY_PASSWORD_NUMBER': False, - 'SECURITY_PASSWORD_SPECIAL_CHAR': False, - 'AUTH_RADIUS': False, - 'RADIUS_SERVER': 'localhost', - 'RADIUS_PORT': 1812, - 'RADIUS_SECRET': '', - 'AUTH_LDAP_SEARCH_PAGED_SIZE': 1000, - 'AUTH_LDAP_SYNC_IS_PERIODIC': False, - 'AUTH_LDAP_SYNC_INTERVAL': None, - 'AUTH_LDAP_SYNC_CRONTAB': None, - 'AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS': False, - 'AUTH_LDAP_OPTIONS_OPT_REFERRALS': -1, - 'HTTP_BIND_HOST': '0.0.0.0', - 'HTTP_LISTEN_PORT': 8080, - 'WS_LISTEN_PORT': 8070, - 'LOGIN_LOG_KEEP_DAYS': 90, - 'ASSETS_PERM_CACHE_TIME': 3600*24, - 'SECURITY_MFA_VERIFY_TTL': 3600, - 'ASSETS_PERM_CACHE_ENABLE': False, - 'SYSLOG_ADDR': '', # '192.168.0.1:514' - 'SYSLOG_FACILITY': 'user', - 'PERM_SINGLE_ASSET_TO_UNGROUP_NODE': False, - 'WINDOWS_SSH_DEFAULT_SHELL': 'cmd', - 'FLOWER_URL': "127.0.0.1:5555", - 'DEFAULT_ORG_SHOW_ALL_USERS': True, - 'PERIOD_TASK_ENABLED': True, - 'WINDOWS_SKIP_ALL_MANUAL_PASSWORD': False, -} - - -def load_from_object(config): - try: - from config import config as c - config.from_object(c) - return True - except ImportError: - pass - return False + You can run `cp config_example.yml config.yml`, and edit it. + """ + raise ImportError(msg) + @classmethod + def get_dynamic_config(cls, config): + return DynamicConfig(config) -def load_from_yml(config): - for i in ['config.yml', 'config.yaml']: - if not os.path.isfile(os.path.join(config.root_path, i)): - continue - loaded = config.from_yaml(i) - if loaded: - return True - return False - - -def load_user_config(): - sys.path.insert(0, PROJECT_DIR) - config = Config(PROJECT_DIR, defaults) - - loaded = load_from_object(config) - if not loaded: - loaded = load_from_yml(config) - if not loaded: - msg = """ - - Error: No config file found. - - You can run `cp config_example.yml config.yml`, and edit it. - """ - raise ImportError(msg) - return config diff --git a/jumpserver/jumpserver/apps/jumpserver/const.py b/jumpserver/jumpserver/apps/jumpserver/const.py index b932d6bb03f2b234226aa57bb60653bbff933d1b..fa33f262ce9fd77041d5eb1cec83a3a96816cab7 100644 --- a/jumpserver/jumpserver/apps/jumpserver/const.py +++ b/jumpserver/jumpserver/apps/jumpserver/const.py @@ -1,3 +1,12 @@ # -*- coding: utf-8 -*- # -VERSION = '1.5.4' +import os +from .conf import ConfigManager + +__all__ = ['BASE_DIR', 'PROJECT_DIR', 'VERSION', 'CONFIG', 'DYNAMIC'] + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PROJECT_DIR = os.path.dirname(BASE_DIR) +VERSION = '1.5.6' +CONFIG = ConfigManager.load_user_config() +DYNAMIC = ConfigManager.get_dynamic_config(CONFIG) diff --git a/jumpserver/jumpserver/apps/jumpserver/context_processor.py b/jumpserver/jumpserver/apps/jumpserver/context_processor.py index 0bd5186ddffac000623add0ea881e50c670528be..0fbef047f4b7e3fbc1cab72bbd2450e041d1425c 100644 --- a/jumpserver/jumpserver/apps/jumpserver/context_processor.py +++ b/jumpserver/jumpserver/apps/jumpserver/context_processor.py @@ -15,10 +15,12 @@ def jumpserver_processor(request): 'FAVICON_URL': static('img/facio.ico'), 'JMS_TITLE': 'Jumpserver', 'VERSION': settings.VERSION, - 'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2019', + 'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2020', 'SECURITY_COMMAND_EXECUTION': settings.SECURITY_COMMAND_EXECUTION, 'SECURITY_MFA_VERIFY_TTL': settings.SECURITY_MFA_VERIFY_TTL, - 'SECURITY_VIEW_AUTH_NEED_MFA': settings.CONFIG.SECURITY_VIEW_AUTH_NEED_MFA, + 'FORCE_SCRIPT_NAME': settings.FORCE_SCRIPT_NAME, + 'SECURITY_VIEW_AUTH_NEED_MFA': settings.SECURITY_VIEW_AUTH_NEED_MFA, + 'LOGIN_CONFIRM_ENABLE': settings.LOGIN_CONFIRM_ENABLE, } return context diff --git a/jumpserver/jumpserver/apps/jumpserver/settings.py b/jumpserver/jumpserver/apps/jumpserver/settings.py deleted file mode 100644 index fe4f2fc849b2688081bdb352be95d5aa1df28d65..0000000000000000000000000000000000000000 --- a/jumpserver/jumpserver/apps/jumpserver/settings.py +++ /dev/null @@ -1,657 +0,0 @@ -""" -Django settings for jumpserver project. - -Generated by 'django-admin startproject' using Django 1.10. - -For more information on this file, see -https://docs.djangoproject.com/en/1.10/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.10/ref/settings/ -""" - -import os -import sys - -import ldap -from django.urls import reverse_lazy - -from . import const -from .conf import load_user_config - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -PROJECT_DIR = os.path.dirname(BASE_DIR) -sys.path.append(PROJECT_DIR) - -CONFIG = load_user_config() -LOG_DIR = os.path.join(PROJECT_DIR, 'logs') -JUMPSERVER_LOG_FILE = os.path.join(LOG_DIR, 'jumpserver.log') -ANSIBLE_LOG_FILE = os.path.join(LOG_DIR, 'ansible.log') -GUNICORN_LOG_FILE = os.path.join(LOG_DIR, 'gunicorn.log') -VERSION = const.VERSION - -if not os.path.isdir(LOG_DIR): - os.makedirs(LOG_DIR) - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = CONFIG.SECRET_KEY - -# SECURITY WARNING: keep the token secret, remove it if all coco, guacamole ok -BOOTSTRAP_TOKEN = CONFIG.BOOTSTRAP_TOKEN - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = CONFIG.DEBUG - -# Absolute url for some case, for example email link -SITE_URL = CONFIG.SITE_URL - -# LOG LEVEL -LOG_LEVEL = CONFIG.LOG_LEVEL - -ALLOWED_HOSTS = ['*'] - -# Max post update field num -DATA_UPLOAD_MAX_NUMBER_FIELDS = 10000 - -# Application definition - -INSTALLED_APPS = [ - 'orgs.apps.OrgsConfig', - 'users.apps.UsersConfig', - 'assets.apps.AssetsConfig', - 'perms.apps.PermsConfig', - 'ops.apps.OpsConfig', - 'settings.apps.SettingsConfig', - 'common.apps.CommonConfig', - 'terminal.apps.TerminalConfig', - 'audits.apps.AuditsConfig', - 'authentication.apps.AuthenticationConfig', # authentication - 'applications.apps.ApplicationsConfig', - 'rest_framework', - 'rest_framework_swagger', - 'drf_yasg', - 'channels', - 'django_filters', - 'bootstrap3', - 'captcha', - 'django_celery_beat', - 'django.contrib.auth', - 'django.contrib.admin', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', -] - - -XPACK_DIR = os.path.join(BASE_DIR, 'xpack') -XPACK_ENABLED = os.path.isdir(XPACK_DIR) -XPACK_TEMPLATES_DIR = [] -XPACK_CONTEXT_PROCESSOR = [] - -if XPACK_ENABLED: - from xpack.utils import get_xpack_templates_dir, get_xpack_context_processor - INSTALLED_APPS.append('xpack.apps.XpackConfig') - XPACK_TEMPLATES_DIR = get_xpack_templates_dir(BASE_DIR) - XPACK_CONTEXT_PROCESSOR = get_xpack_context_processor() - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'authentication.backends.openid.middleware.OpenIDAuthenticationMiddleware', - 'jumpserver.middleware.TimezoneMiddleware', - 'jumpserver.middleware.DemoMiddleware', - 'jumpserver.middleware.RequestMiddleware', - 'orgs.middleware.OrgMiddleware', -] - - -ROOT_URLCONF = 'jumpserver.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'templates'), *XPACK_TEMPLATES_DIR], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.i18n', - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - 'django.template.context_processors.static', - 'django.template.context_processors.request', - 'django.template.context_processors.media', - 'jumpserver.context_processor.jumpserver_processor', - 'orgs.context_processor.org_processor', - *XPACK_CONTEXT_PROCESSOR, - ], - }, - }, -] - -WSGI_APPLICATION = 'jumpserver.wsgi.application' -ASGI_APPLICATION = 'jumpserver.routing.application' - -LOGIN_REDIRECT_URL = reverse_lazy('index') -LOGIN_URL = reverse_lazy('authentication:login') - -SESSION_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN -CSRF_COOKIE_DOMAIN = CONFIG.CSRF_COOKIE_DOMAIN -SESSION_COOKIE_AGE = CONFIG.SESSION_COOKIE_AGE -SESSION_EXPIRE_AT_BROWSER_CLOSE = CONFIG.SESSION_EXPIRE_AT_BROWSER_CLOSE -SESSION_ENGINE = 'redis_sessions.session' -SESSION_REDIS = { - 'host': CONFIG.REDIS_HOST, - 'port': CONFIG.REDIS_PORT, - 'password': CONFIG.REDIS_PASSWORD, - 'db': CONFIG.REDIS_DB_SESSION, - 'prefix': 'auth_session', - 'socket_timeout': 1, - 'retry_on_timeout': False -} - -MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' -# Database -# https://docs.djangoproject.com/en/1.10/ref/settings/#databases - -DB_OPTIONS = {} -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.{}'.format(CONFIG.DB_ENGINE.lower()), - 'NAME': CONFIG.DB_NAME, - 'HOST': CONFIG.DB_HOST, - 'PORT': CONFIG.DB_PORT, - 'USER': CONFIG.DB_USER, - 'PASSWORD': CONFIG.DB_PASSWORD, - 'ATOMIC_REQUESTS': True, - 'OPTIONS': DB_OPTIONS - } -} -DB_CA_PATH = os.path.join(PROJECT_DIR, 'data', 'certs', 'db_ca.pem') -if CONFIG.DB_ENGINE.lower() == 'mysql': - DB_OPTIONS['init_command'] = "SET sql_mode='STRICT_TRANS_TABLES'" - if os.path.isfile(DB_CA_PATH): - DB_OPTIONS['ssl'] = {'ca': DB_CA_PATH} - - -# Password validation -# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators -# -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - -# Logging setting -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'verbose': { - 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' - }, - 'main': { - 'datefmt': '%Y-%m-%d %H:%M:%S', - 'format': '%(asctime)s [%(module)s %(levelname)s] %(message)s', - }, - 'simple': { - 'format': '%(levelname)s %(message)s' - }, - 'syslog': { - 'format': 'jumpserver: %(message)s' - }, - 'msg': { - 'format': '%(message)s' - } - }, - 'handlers': { - 'null': { - 'level': 'DEBUG', - 'class': 'logging.NullHandler', - }, - 'console': { - 'level': 'DEBUG', - 'class': 'logging.StreamHandler', - 'formatter': 'main' - }, - 'file': { - 'encoding': 'utf8', - 'level': 'DEBUG', - 'class': 'logging.handlers.RotatingFileHandler', - 'maxBytes': 1024*1024*100, - 'backupCount': 7, - 'formatter': 'main', - 'filename': JUMPSERVER_LOG_FILE, - }, - 'ansible_logs': { - 'encoding': 'utf8', - 'level': 'DEBUG', - 'class': 'logging.handlers.RotatingFileHandler', - 'formatter': 'main', - 'maxBytes': 1024*1024*100, - 'backupCount': 7, - 'filename': ANSIBLE_LOG_FILE, - }, - 'syslog': { - 'level': 'INFO', - 'class': 'logging.NullHandler', - 'formatter': 'syslog' - }, - }, - 'loggers': { - 'django': { - 'handlers': ['null'], - 'propagate': False, - 'level': LOG_LEVEL, - }, - 'django.request': { - 'handlers': ['console', 'file', 'syslog'], - 'level': LOG_LEVEL, - 'propagate': False, - }, - 'django.server': { - 'handlers': ['console', 'file', 'syslog'], - 'level': LOG_LEVEL, - 'propagate': False, - }, - 'jumpserver': { - 'handlers': ['console', 'file', 'syslog'], - 'level': LOG_LEVEL, - }, - 'ops.ansible_api': { - 'handlers': ['console', 'ansible_logs'], - 'level': LOG_LEVEL, - }, - 'django_auth_ldap': { - 'handlers': ['console', 'file'], - 'level': "INFO", - }, - 'jms.audits': { - 'handlers': ['syslog'], - 'level': 'INFO' - }, - # 'django.db': { - # 'handlers': ['console', 'file'], - # 'level': 'DEBUG' - # } - } -} - -SYSLOG_ENABLE = False - -if CONFIG.SYSLOG_ADDR != '' and len(CONFIG.SYSLOG_ADDR.split(':')) == 2: - host, port = CONFIG.SYSLOG_ADDR.split(':') - SYSLOG_ENABLE = True - LOGGING['handlers']['syslog'].update({ - 'class': 'logging.handlers.SysLogHandler', - 'facility': CONFIG.SYSLOG_FACILITY, - 'address': (host, int(port)), - }) - -# Internationalization -# https://docs.djangoproject.com/en/1.10/topics/i18n/ -# LANGUAGE_CODE = 'en' -LANGUAGE_CODE = 'zh' - -TIME_ZONE = 'Asia/Shanghai' - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - -# I18N translation -LOCALE_PATHS = [ - os.path.join(BASE_DIR, 'locale'), -] - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.10/howto/static-files/ - -STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(PROJECT_DIR, "data", "static") -STATIC_DIR = os.path.join(BASE_DIR, "static") - -STATICFILES_DIRS = ( - os.path.join(BASE_DIR, "static"), -) - -# Media files (File, ImageField) will be save these - -MEDIA_URL = '/media/' - -MEDIA_ROOT = os.path.join(PROJECT_DIR, 'data', 'media').replace('\\', '/') + '/' - -# Use django-bootstrap-form to format template, input max width arg -# BOOTSTRAP_COLUMN_COUNT = 11 - -# Init data or generate fake data source for development -FIXTURE_DIRS = [os.path.join(BASE_DIR, 'fixtures'), ] - -# Email config -EMAIL_HOST = 'smtp.jumpserver.org' -EMAIL_PORT = 25 -EMAIL_HOST_USER = 'noreply@jumpserver.org' -EMAIL_HOST_PASSWORD = '' -EMAIL_FROM = '' -EMAIL_RECIPIENT = '' -EMAIL_USE_SSL = False -EMAIL_USE_TLS = False -EMAIL_SUBJECT_PREFIX = '[JMS] ' - -# Email custom content -EMAIL_CUSTOM_USER_CREATED_SUBJECT = '' -EMAIL_CUSTOM_USER_CREATED_HONORIFIC = '' -EMAIL_CUSTOM_USER_CREATED_BODY = '' -EMAIL_CUSTOM_USER_CREATED_SIGNATURE = '' - -REST_FRAMEWORK = { - # Use Django's standard `django.contrib.auth` permissions, - # or allow read-only access for unauthenticated users. - 'DEFAULT_PERMISSION_CLASSES': ( - 'common.permissions.IsOrgAdmin', - ), - 'DEFAULT_RENDERER_CLASSES': ( - 'rest_framework.renderers.JSONRenderer', - 'rest_framework.renderers.BrowsableAPIRenderer', - 'common.renders.JMSCSVRender', - ), - 'DEFAULT_PARSER_CLASSES': ( - 'rest_framework.parsers.JSONParser', - 'rest_framework.parsers.FormParser', - 'rest_framework.parsers.MultiPartParser', - 'common.parsers.JMSCSVParser', - 'rest_framework.parsers.FileUploadParser', - ), - 'DEFAULT_AUTHENTICATION_CLASSES': ( - # 'rest_framework.authentication.BasicAuthentication', - 'authentication.backends.api.AccessKeyAuthentication', - 'authentication.backends.api.AccessTokenAuthentication', - 'authentication.backends.api.PrivateTokenAuthentication', - 'authentication.backends.api.SignatureAuthentication', - 'authentication.backends.api.SessionAuthentication', - ), - 'DEFAULT_FILTER_BACKENDS': ( - 'django_filters.rest_framework.DjangoFilterBackend', - 'rest_framework.filters.SearchFilter', - 'rest_framework.filters.OrderingFilter', - ), - 'DEFAULT_METADATA_CLASS': 'common.drfmetadata.SimpleMetadataWithFilters', - 'ORDERING_PARAM': "order", - 'SEARCH_PARAM': "search", - 'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S %z', - 'DATETIME_INPUT_FORMATS': ['iso-8601', '%Y-%m-%d %H:%M:%S %z'], - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', - # 'PAGE_SIZE': 15 -} - -AUTHENTICATION_BACKENDS = [ - 'django.contrib.auth.backends.ModelBackend', -] - -# Custom User Auth model -AUTH_USER_MODEL = 'users.User' - -# File Upload Permissions -FILE_UPLOAD_PERMISSIONS = 0o644 -FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o755 - -# OTP settings -OTP_ISSUER_NAME = CONFIG.OTP_ISSUER_NAME -OTP_VALID_WINDOW = CONFIG.OTP_VALID_WINDOW - -# Auth LDAP settings -AUTH_LDAP = False -AUTH_LDAP_SEARCH_PAGED_SIZE = CONFIG.AUTH_LDAP_SEARCH_PAGED_SIZE -AUTH_LDAP_SYNC_IS_PERIODIC = CONFIG.AUTH_LDAP_SYNC_IS_PERIODIC -AUTH_LDAP_SYNC_INTERVAL = CONFIG.AUTH_LDAP_SYNC_INTERVAL -AUTH_LDAP_SYNC_CRONTAB = CONFIG.AUTH_LDAP_SYNC_CRONTAB -AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS = CONFIG.AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS - -AUTH_LDAP_SERVER_URI = 'ldap://localhost:389' -AUTH_LDAP_BIND_DN = 'cn=admin,dc=jumpserver,dc=org' -AUTH_LDAP_BIND_PASSWORD = '' -AUTH_LDAP_SEARCH_OU = 'ou=tech,dc=jumpserver,dc=org' -AUTH_LDAP_SEARCH_FILTER = '(cn=%(user)s)' -AUTH_LDAP_START_TLS = False -AUTH_LDAP_USER_ATTR_MAP = {"username": "cn", "name": "sn", "email": "mail"} -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") -if os.path.isfile(LDAP_CERT_FILE): - AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_X_TLS_CACERTFILE] = LDAP_CERT_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( -# AUTH_LDAP_GROUP_SEARCH_OU, ldap.SCOPE_SUBTREE, AUTH_LDAP_GROUP_SEARCH_FILTER -# ) -AUTH_LDAP_CONNECTION_OPTIONS = { - ldap.OPT_TIMEOUT: 30 -} -AUTH_LDAP_GROUP_CACHE_TIMEOUT = 1 -AUTH_LDAP_ALWAYS_UPDATE_USER = True -AUTH_LDAP_BACKEND = 'authentication.backends.ldap.LDAPAuthorizationBackend' - -if AUTH_LDAP: - AUTHENTICATION_BACKENDS.insert(0, AUTH_LDAP_BACKEND) - -# openid -# Auth OpenID settings -BASE_SITE_URL = CONFIG.BASE_SITE_URL -AUTH_OPENID = CONFIG.AUTH_OPENID -AUTH_OPENID_SERVER_URL = CONFIG.AUTH_OPENID_SERVER_URL -AUTH_OPENID_REALM_NAME = CONFIG.AUTH_OPENID_REALM_NAME -AUTH_OPENID_CLIENT_ID = CONFIG.AUTH_OPENID_CLIENT_ID -AUTH_OPENID_CLIENT_SECRET = CONFIG.AUTH_OPENID_CLIENT_SECRET -AUTH_OPENID_IGNORE_SSL_VERIFICATION = CONFIG.AUTH_OPENID_IGNORE_SSL_VERIFICATION -AUTH_OPENID_SHARE_SESSION = CONFIG.AUTH_OPENID_SHARE_SESSION -AUTH_OPENID_BACKENDS = [ - 'authentication.backends.openid.backends.OpenIDAuthorizationPasswordBackend', - 'authentication.backends.openid.backends.OpenIDAuthorizationCodeBackend', -] - -if AUTH_OPENID: - LOGIN_URL = reverse_lazy("authentication:openid:openid-login") - LOGIN_COMPLETE_URL = reverse_lazy("authentication:openid:openid-login-complete") - AUTHENTICATION_BACKENDS.insert(0, AUTH_OPENID_BACKENDS[0]) - AUTHENTICATION_BACKENDS.insert(0, AUTH_OPENID_BACKENDS[1]) - -# Radius Auth -AUTH_RADIUS = CONFIG.AUTH_RADIUS -AUTH_RADIUS_BACKEND = 'authentication.backends.radius.RadiusBackend' -RADIUS_SERVER = CONFIG.RADIUS_SERVER -RADIUS_PORT = CONFIG.RADIUS_PORT -RADIUS_SECRET = CONFIG.RADIUS_SECRET - -if AUTH_RADIUS: - AUTHENTICATION_BACKENDS.insert(0, AUTH_RADIUS_BACKEND) - -# Dump all celery log to here -CELERY_LOG_DIR = os.path.join(PROJECT_DIR, 'data', 'celery') - -# Celery using redis as broker -CELERY_BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % { - 'password': CONFIG.REDIS_PASSWORD, - 'host': CONFIG.REDIS_HOST, - 'port': CONFIG.REDIS_PORT, - 'db': CONFIG.REDIS_DB_CELERY, -} -CELERY_TASK_SERIALIZER = 'pickle' -CELERY_RESULT_SERIALIZER = 'pickle' -CELERY_RESULT_BACKEND = CELERY_BROKER_URL -CELERY_ACCEPT_CONTENT = ['json', 'pickle'] -CELERY_RESULT_EXPIRES = 3600 -# CELERY_WORKER_LOG_FORMAT = '%(asctime)s [%(module)s %(levelname)s] %(message)s' -# CELERY_WORKER_LOG_FORMAT = '%(message)s' -# CELERY_WORKER_TASK_LOG_FORMAT = '%(task_id)s %(task_name)s %(message)s' -CELERY_WORKER_TASK_LOG_FORMAT = '%(message)s' -# CELERY_WORKER_LOG_FORMAT = '%(asctime)s [%(module)s %(levelname)s] %(message)s' -CELERY_WORKER_LOG_FORMAT = '%(message)s' -CELERY_TASK_EAGER_PROPAGATES = True -CELERY_WORKER_REDIRECT_STDOUTS = True -CELERY_WORKER_REDIRECT_STDOUTS_LEVEL = "INFO" -# CELERY_WORKER_HIJACK_ROOT_LOGGER = True -# CELERY_WORKER_MAX_TASKS_PER_CHILD = 40 -CELERY_TASK_SOFT_TIME_LIMIT = 3600 - -# Cache use redis -CACHES = { - 'default': { - 'BACKEND': 'redis_cache.RedisCache', - 'LOCATION': 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % { - 'password': CONFIG.REDIS_PASSWORD, - 'host': CONFIG.REDIS_HOST, - 'port': CONFIG.REDIS_PORT, - 'db': CONFIG.REDIS_DB_CACHE, - } - } -} - -# Captcha settings, more see https://django-simple-captcha.readthedocs.io/en/latest/advanced.html -CAPTCHA_IMAGE_SIZE = (80, 33) -CAPTCHA_FOREGROUND_COLOR = '#001100' -CAPTCHA_NOISE_FUNCTIONS = ('captcha.helpers.noise_dots',) -CAPTCHA_TEST_MODE = CONFIG.CAPTCHA_TEST_MODE - -COMMAND_STORAGE = { - 'ENGINE': 'terminal.backends.command.db', -} - -DEFAULT_TERMINAL_COMMAND_STORAGE = { - "default": { - "TYPE": "server", - }, -} - -TERMINAL_COMMAND_STORAGE = CONFIG.TERMINAL_COMMAND_STORAGE - -DEFAULT_TERMINAL_REPLAY_STORAGE = { - "default": { - "TYPE": "server", - }, -} - -TERMINAL_REPLAY_STORAGE = { -} - - -SECURITY_MFA_AUTH = False -SECURITY_COMMAND_EXECUTION = True -SECURITY_LOGIN_LIMIT_COUNT = 7 -SECURITY_LOGIN_LIMIT_TIME = 30 # Unit: minute -SECURITY_MAX_IDLE_TIME = 30 # Unit: minute -SECURITY_PASSWORD_EXPIRATION_TIME = 9999 # Unit: day -SECURITY_PASSWORD_MIN_LENGTH = 6 # Unit: bit -SECURITY_PASSWORD_UPPER_CASE = False -SECURITY_PASSWORD_LOWER_CASE = False -SECURITY_PASSWORD_NUMBER = False -SECURITY_PASSWORD_SPECIAL_CHAR = False -SECURITY_PASSWORD_RULES = [ - 'SECURITY_PASSWORD_MIN_LENGTH', - 'SECURITY_PASSWORD_UPPER_CASE', - 'SECURITY_PASSWORD_LOWER_CASE', - 'SECURITY_PASSWORD_NUMBER', - 'SECURITY_PASSWORD_SPECIAL_CHAR' -] -SECURITY_MFA_VERIFY_TTL = CONFIG.SECURITY_MFA_VERIFY_TTL -SECURITY_SERVICE_ACCOUNT_REGISTRATION = CONFIG.SECURITY_SERVICE_ACCOUNT_REGISTRATION -TERMINAL_PASSWORD_AUTH = CONFIG.TERMINAL_PASSWORD_AUTH -TERMINAL_PUBLIC_KEY_AUTH = CONFIG.TERMINAL_PUBLIC_KEY_AUTH -TERMINAL_HEARTBEAT_INTERVAL = CONFIG.TERMINAL_HEARTBEAT_INTERVAL -TERMINAL_ASSET_LIST_SORT_BY = CONFIG.TERMINAL_ASSET_LIST_SORT_BY -TERMINAL_ASSET_LIST_PAGE_SIZE = CONFIG.TERMINAL_ASSET_LIST_PAGE_SIZE -TERMINAL_SESSION_KEEP_DURATION = CONFIG.TERMINAL_SESSION_KEEP_DURATION -TERMINAL_HOST_KEY = CONFIG.TERMINAL_HOST_KEY -TERMINAL_HEADER_TITLE = CONFIG.TERMINAL_HEADER_TITLE -TERMINAL_TELNET_REGEX = CONFIG.TERMINAL_TELNET_REGEX - -# Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html -BOOTSTRAP3 = { - 'horizontal_label_class': 'col-md-2', - # Field class to use in horizontal forms - 'horizontal_field_class': 'col-md-9', - # Set placeholder attributes to label if no placeholder is provided - 'set_placeholder': False, - 'success_css_class': '', - 'required_css_class': 'required', -} - -TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION -DISPLAY_PER_PAGE = CONFIG.DISPLAY_PER_PAGE -DEFAULT_EXPIRED_YEARS = 70 -USER_GUIDE_URL = "" - - -SWAGGER_SETTINGS = { - 'DEFAULT_AUTO_SCHEMA_CLASS': 'jumpserver.swagger.CustomSwaggerAutoSchema', - 'USE_SESSION_AUTH': True, - 'SECURITY_DEFINITIONS': { - 'Bearer': { - 'type': 'apiKey', - 'name': 'Authorization', - 'in': 'header' - } - }, -} - - -# Default email suffix -EMAIL_SUFFIX = CONFIG.EMAIL_SUFFIX -LOGIN_LOG_KEEP_DAYS = CONFIG.LOGIN_LOG_KEEP_DAYS - -# User or user group permission cache time, default 3600 seconds -ASSETS_PERM_CACHE_ENABLE = CONFIG.ASSETS_PERM_CACHE_ENABLE -ASSETS_PERM_CACHE_TIME = CONFIG.ASSETS_PERM_CACHE_TIME - -# Asset user auth external backend, default AuthBook backend -BACKEND_ASSET_USER_AUTH_VAULT = False - -DEFAULT_ORG_SHOW_ALL_USERS = CONFIG.DEFAULT_ORG_SHOW_ALL_USERS - -PERM_SINGLE_ASSET_TO_UNGROUP_NODE = CONFIG.PERM_SINGLE_ASSET_TO_UNGROUP_NODE -WINDOWS_SSH_DEFAULT_SHELL = CONFIG.WINDOWS_SSH_DEFAULT_SHELL -FLOWER_URL = CONFIG.FLOWER_URL - - -# Django channels support websocket -CHANNEL_REDIS = "redis://:{}@{}:{}/{}".format( - CONFIG.REDIS_PASSWORD, CONFIG.REDIS_HOST, CONFIG.REDIS_PORT, - CONFIG.REDIS_DB_WS, -) - -CHANNEL_LAYERS = { - 'default': { - 'BACKEND': 'channels_redis.core.RedisChannelLayer', - 'CONFIG': { - "hosts": [CHANNEL_REDIS], - }, - }, -} - -# Enable internal period task -PERIOD_TASK_ENABLED = CONFIG.PERIOD_TASK_ENABLED diff --git a/jumpserver/jumpserver/apps/jumpserver/settings/__init__.py b/jumpserver/jumpserver/apps/jumpserver/settings/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a82b2ceb1ad8de069f6717537ac88d8bbd5cabc9 --- /dev/null +++ b/jumpserver/jumpserver/apps/jumpserver/settings/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# +from .base import * +from .logging import * +from .libs import * +from .auth import * +from .custom import * +from ._xpack import * diff --git a/jumpserver/jumpserver/apps/jumpserver/settings/_xpack.py b/jumpserver/jumpserver/apps/jumpserver/settings/_xpack.py new file mode 100644 index 0000000000000000000000000000000000000000..8957d83c49df2b40ee051cb4297dfeb7b41f2f24 --- /dev/null +++ b/jumpserver/jumpserver/apps/jumpserver/settings/_xpack.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# + +import os +from .. import const +from .base import INSTALLED_APPS, TEMPLATES + +XPACK_DIR = os.path.join(const.BASE_DIR, 'xpack') +XPACK_ENABLED = os.path.isdir(XPACK_DIR) +XPACK_TEMPLATES_DIR = [] +XPACK_CONTEXT_PROCESSOR = [] + +if XPACK_ENABLED: + from xpack.utils import get_xpack_templates_dir, get_xpack_context_processor + INSTALLED_APPS.append('xpack.apps.XpackConfig') + XPACK_TEMPLATES_DIR = get_xpack_templates_dir(const.BASE_DIR) + XPACK_CONTEXT_PROCESSOR = get_xpack_context_processor() + TEMPLATES[0]['DIRS'].extend(XPACK_TEMPLATES_DIR) + TEMPLATES[0]['OPTIONS']['context_processors'].extend(XPACK_CONTEXT_PROCESSOR) diff --git a/jumpserver/jumpserver/apps/jumpserver/settings/auth.py b/jumpserver/jumpserver/apps/jumpserver/settings/auth.py new file mode 100644 index 0000000000000000000000000000000000000000..978faa751a4d85a7de25a1f2c425ae4e67b59fa1 --- /dev/null +++ b/jumpserver/jumpserver/apps/jumpserver/settings/auth.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# +import os +import ldap +from django.urls import reverse_lazy + +from ..const import CONFIG, DYNAMIC, PROJECT_DIR + +# OTP settings +OTP_ISSUER_NAME = CONFIG.OTP_ISSUER_NAME +OTP_VALID_WINDOW = CONFIG.OTP_VALID_WINDOW + +# Auth LDAP settings +AUTH_LDAP = DYNAMIC.AUTH_LDAP +AUTH_LDAP_SERVER_URI = DYNAMIC.AUTH_LDAP_SERVER_URI +AUTH_LDAP_BIND_DN = DYNAMIC.AUTH_LDAP_BIND_DN +AUTH_LDAP_BIND_PASSWORD = DYNAMIC.AUTH_LDAP_BIND_PASSWORD +AUTH_LDAP_SEARCH_OU = DYNAMIC.AUTH_LDAP_SEARCH_OU +AUTH_LDAP_SEARCH_FILTER = DYNAMIC.AUTH_LDAP_SEARCH_FILTER +AUTH_LDAP_START_TLS = DYNAMIC.AUTH_LDAP_START_TLS +AUTH_LDAP_USER_ATTR_MAP = DYNAMIC.AUTH_LDAP_USER_ATTR_MAP +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") +if os.path.isfile(LDAP_CERT_FILE): + AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_X_TLS_CACERTFILE] = LDAP_CERT_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( +# AUTH_LDAP_GROUP_SEARCH_OU, ldap.SCOPE_SUBTREE, AUTH_LDAP_GROUP_SEARCH_FILTER +# ) +AUTH_LDAP_CONNECTION_OPTIONS = { + ldap.OPT_TIMEOUT: CONFIG.AUTH_LDAP_CONNECT_TIMEOUT +} +AUTH_LDAP_CACHE_TIMEOUT = 1 +AUTH_LDAP_ALWAYS_UPDATE_USER = True + +AUTH_LDAP_SEARCH_PAGED_SIZE = CONFIG.AUTH_LDAP_SEARCH_PAGED_SIZE +AUTH_LDAP_SYNC_IS_PERIODIC = CONFIG.AUTH_LDAP_SYNC_IS_PERIODIC +AUTH_LDAP_SYNC_INTERVAL = CONFIG.AUTH_LDAP_SYNC_INTERVAL +AUTH_LDAP_SYNC_CRONTAB = CONFIG.AUTH_LDAP_SYNC_CRONTAB +AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS = CONFIG.AUTH_LDAP_USER_LOGIN_ONLY_IN_USERS + +# openid +# Auth OpenID settings +BASE_SITE_URL = CONFIG.BASE_SITE_URL +AUTH_OPENID = CONFIG.AUTH_OPENID +AUTH_OPENID_SERVER_URL = CONFIG.AUTH_OPENID_SERVER_URL +AUTH_OPENID_REALM_NAME = CONFIG.AUTH_OPENID_REALM_NAME +AUTH_OPENID_CLIENT_ID = CONFIG.AUTH_OPENID_CLIENT_ID +AUTH_OPENID_CLIENT_SECRET = CONFIG.AUTH_OPENID_CLIENT_SECRET +AUTH_OPENID_IGNORE_SSL_VERIFICATION = CONFIG.AUTH_OPENID_IGNORE_SSL_VERIFICATION +AUTH_OPENID_SHARE_SESSION = CONFIG.AUTH_OPENID_SHARE_SESSION +AUTH_OPENID_LOGIN_URL = reverse_lazy("authentication:openid:openid-login") +AUTH_OPENID_LOGIN_COMPLETE_URL = reverse_lazy("authentication:openid:openid-login-complete") + + +# Radius Auth +AUTH_RADIUS = CONFIG.AUTH_RADIUS +AUTH_RADIUS_BACKEND = 'authentication.backends.radius.RadiusBackend' +RADIUS_SERVER = CONFIG.RADIUS_SERVER +RADIUS_PORT = CONFIG.RADIUS_PORT +RADIUS_SECRET = CONFIG.RADIUS_SECRET + + +TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION +LOGIN_CONFIRM_ENABLE = CONFIG.LOGIN_CONFIRM_ENABLE +OTP_IN_RADIUS = CONFIG.OTP_IN_RADIUS diff --git a/jumpserver/jumpserver/apps/jumpserver/settings/base.py b/jumpserver/jumpserver/apps/jumpserver/settings/base.py new file mode 100644 index 0000000000000000000000000000000000000000..49425b488ba25f89d3ee88b61c70e6b4d5c6e4e9 --- /dev/null +++ b/jumpserver/jumpserver/apps/jumpserver/settings/base.py @@ -0,0 +1,247 @@ +import os + +from django.urls import reverse_lazy + +from .. import const +from ..const import CONFIG, DYNAMIC + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +VERSION = const.VERSION +BASE_DIR = const.BASE_DIR +PROJECT_DIR = const.PROJECT_DIR + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = CONFIG.SECRET_KEY + +# SECURITY WARNING: keep the token secret, remove it if all coco, guacamole ok +BOOTSTRAP_TOKEN = CONFIG.BOOTSTRAP_TOKEN + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = CONFIG.DEBUG + +# Absolute url for some case, for example email link +SITE_URL = DYNAMIC.SITE_URL + +# LOG LEVEL +LOG_LEVEL = CONFIG.LOG_LEVEL + +ALLOWED_HOSTS = ['*'] + +# Max post update field num +DATA_UPLOAD_MAX_NUMBER_FIELDS = 10000 + +# Application definition + +INSTALLED_APPS = [ + 'orgs.apps.OrgsConfig', + 'users.apps.UsersConfig', + 'assets.apps.AssetsConfig', + 'perms.apps.PermsConfig', + 'ops.apps.OpsConfig', + 'settings.apps.SettingsConfig', + 'common.apps.CommonConfig', + 'terminal.apps.TerminalConfig', + 'audits.apps.AuditsConfig', + 'authentication.apps.AuthenticationConfig', # authentication + 'applications.apps.ApplicationsConfig', + 'tickets.apps.TicketsConfig', + 'rest_framework', + 'rest_framework_swagger', + 'drf_yasg', + 'channels', + 'django_filters', + 'bootstrap3', + 'captcha', + 'django_celery_beat', + 'django.contrib.auth', + 'django.contrib.admin', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'authentication.backends.openid.middleware.OpenIDAuthenticationMiddleware', + 'jumpserver.middleware.TimezoneMiddleware', + 'jumpserver.middleware.DemoMiddleware', + 'jumpserver.middleware.RequestMiddleware', + 'orgs.middleware.OrgMiddleware', +] + + +ROOT_URLCONF = 'jumpserver.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'templates')], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.i18n', + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'django.template.context_processors.static', + 'django.template.context_processors.request', + 'django.template.context_processors.media', + 'jumpserver.context_processor.jumpserver_processor', + 'orgs.context_processor.org_processor', + ], + }, + }, +] + +WSGI_APPLICATION = 'jumpserver.wsgi.application' + +LOGIN_REDIRECT_URL = reverse_lazy('index') +LOGIN_URL = reverse_lazy('authentication:login') + +SESSION_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN +CSRF_COOKIE_DOMAIN = CONFIG.CSRF_COOKIE_DOMAIN +SESSION_COOKIE_AGE = CONFIG.SESSION_COOKIE_AGE +SESSION_EXPIRE_AT_BROWSER_CLOSE = CONFIG.SESSION_EXPIRE_AT_BROWSER_CLOSE +SESSION_ENGINE = 'redis_sessions.session' +SESSION_REDIS = { + 'host': CONFIG.REDIS_HOST, + 'port': CONFIG.REDIS_PORT, + 'password': CONFIG.REDIS_PASSWORD, + 'db': CONFIG.REDIS_DB_SESSION, + 'prefix': 'auth_session', + 'socket_timeout': 1, + 'retry_on_timeout': False +} + +MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' +# Database +# https://docs.djangoproject.com/en/1.10/ref/settings/#databases + +DB_OPTIONS = {} +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.{}'.format(CONFIG.DB_ENGINE.lower()), + 'NAME': CONFIG.DB_NAME, + 'HOST': CONFIG.DB_HOST, + 'PORT': CONFIG.DB_PORT, + 'USER': CONFIG.DB_USER, + 'PASSWORD': CONFIG.DB_PASSWORD, + 'ATOMIC_REQUESTS': True, + 'OPTIONS': DB_OPTIONS + } +} +DB_CA_PATH = os.path.join(PROJECT_DIR, 'data', 'certs', 'db_ca.pem') +if CONFIG.DB_ENGINE.lower() == 'mysql': + DB_OPTIONS['init_command'] = "SET sql_mode='STRICT_TRANS_TABLES'" + if os.path.isfile(DB_CA_PATH): + DB_OPTIONS['ssl'] = {'ca': DB_CA_PATH} + + +# Password validation +# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators +# +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +# Internationalization +# https://docs.djangoproject.com/en/1.10/topics/i18n/ +# LANGUAGE_CODE = 'en' +LANGUAGE_CODE = 'zh' + +TIME_ZONE = 'Asia/Shanghai' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + +# I18N translation +LOCALE_PATHS = [ + os.path.join(BASE_DIR, 'locale'), +] + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.10/howto/static-files/ + +STATIC_URL = '{}/static/'.format(CONFIG.FORCE_SCRIPT_NAME) +STATIC_ROOT = os.path.join(PROJECT_DIR, "data", "static") +STATIC_DIR = os.path.join(BASE_DIR, "static") + +STATICFILES_DIRS = ( + os.path.join(BASE_DIR, "static"), +) + +# Media files (File, ImageField) will be save these + +MEDIA_URL = '/media/' + +MEDIA_ROOT = os.path.join(PROJECT_DIR, 'data', 'media').replace('\\', '/') + '/' + +# Use django-bootstrap-form to format template, input max width arg +# BOOTSTRAP_COLUMN_COUNT = 11 + +# Init data or generate fake data source for development +FIXTURE_DIRS = [os.path.join(BASE_DIR, 'fixtures'), ] + +# Email config +EMAIL_HOST = DYNAMIC.EMAIL_HOST +EMAIL_PORT = DYNAMIC.EMAIL_PORT +EMAIL_HOST_USER = DYNAMIC.EMAIL_HOST_USER +EMAIL_HOST_PASSWORD = DYNAMIC.EMAIL_HOST_PASSWORD +EMAIL_FROM = DYNAMIC.EMAIL_FROM +EMAIL_RECIPIENT = DYNAMIC.EMAIL_RECIPIENT +EMAIL_USE_SSL = DYNAMIC.EMAIL_USE_SSL +EMAIL_USE_TLS = DYNAMIC.EMAIL_USE_TLS + + +AUTHENTICATION_BACKENDS = DYNAMIC.AUTHENTICATION_BACKENDS + +# Custom User Auth model +AUTH_USER_MODEL = 'users.User' + +# File Upload Permissions +FILE_UPLOAD_PERMISSIONS = 0o644 +FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o755 + +# Cache use redis +CACHES = { + 'default': { + 'BACKEND': 'redis_cache.RedisCache', + 'LOCATION': 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % { + 'password': CONFIG.REDIS_PASSWORD, + 'host': CONFIG.REDIS_HOST, + 'port': CONFIG.REDIS_PORT, + 'db': CONFIG.REDIS_DB_CACHE, + } + } +} + + +FORCE_SCRIPT_NAME = CONFIG.FORCE_SCRIPT_NAME + diff --git a/jumpserver/jumpserver/apps/jumpserver/settings/custom.py b/jumpserver/jumpserver/apps/jumpserver/settings/custom.py new file mode 100644 index 0000000000000000000000000000000000000000..0a910f984298e27afbc0adab37a69bac9b65cce8 --- /dev/null +++ b/jumpserver/jumpserver/apps/jumpserver/settings/custom.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# +from ..const import CONFIG, DYNAMIC + +# Storage settings +COMMAND_STORAGE = { + 'ENGINE': 'terminal.backends.command.db', +} +DEFAULT_TERMINAL_COMMAND_STORAGE = { + "default": { + "TYPE": "server", + }, +} +TERMINAL_COMMAND_STORAGE = DYNAMIC.TERMINAL_COMMAND_STORAGE or {} +DEFAULT_TERMINAL_REPLAY_STORAGE = { + "default": { + "TYPE": "server", + }, +} +TERMINAL_REPLAY_STORAGE = DYNAMIC.TERMINAL_REPLAY_STORAGE + +# Security settings +SECURITY_MFA_AUTH = DYNAMIC.SECURITY_MFA_AUTH +SECURITY_COMMAND_EXECUTION = DYNAMIC.SECURITY_COMMAND_EXECUTION +SECURITY_LOGIN_LIMIT_COUNT = DYNAMIC.SECURITY_LOGIN_LIMIT_COUNT +SECURITY_LOGIN_LIMIT_TIME = DYNAMIC.SECURITY_LOGIN_LIMIT_TIME # Unit: minute +SECURITY_MAX_IDLE_TIME = DYNAMIC.SECURITY_MAX_IDLE_TIME # Unit: minute +SECURITY_PASSWORD_EXPIRATION_TIME = DYNAMIC.SECURITY_PASSWORD_EXPIRATION_TIME # Unit: day +SECURITY_PASSWORD_MIN_LENGTH = DYNAMIC.SECURITY_PASSWORD_MIN_LENGTH # Unit: bit +SECURITY_PASSWORD_UPPER_CASE = DYNAMIC.SECURITY_PASSWORD_UPPER_CASE +SECURITY_PASSWORD_LOWER_CASE = DYNAMIC.SECURITY_PASSWORD_LOWER_CASE +SECURITY_PASSWORD_NUMBER = DYNAMIC.SECURITY_PASSWORD_NUMBER +SECURITY_PASSWORD_SPECIAL_CHAR = DYNAMIC.SECURITY_PASSWORD_SPECIAL_CHAR +SECURITY_PASSWORD_RULES = [ + 'SECURITY_PASSWORD_MIN_LENGTH', + 'SECURITY_PASSWORD_UPPER_CASE', + 'SECURITY_PASSWORD_LOWER_CASE', + 'SECURITY_PASSWORD_NUMBER', + 'SECURITY_PASSWORD_SPECIAL_CHAR' +] +SECURITY_MFA_VERIFY_TTL = CONFIG.SECURITY_MFA_VERIFY_TTL +SECURITY_VIEW_AUTH_NEED_MFA = CONFIG.SECURITY_VIEW_AUTH_NEED_MFA +SECURITY_SERVICE_ACCOUNT_REGISTRATION = DYNAMIC.SECURITY_SERVICE_ACCOUNT_REGISTRATION + +# Terminal other setting +TERMINAL_PASSWORD_AUTH = DYNAMIC.TERMINAL_PASSWORD_AUTH +TERMINAL_PUBLIC_KEY_AUTH = DYNAMIC.TERMINAL_PUBLIC_KEY_AUTH +TERMINAL_HEARTBEAT_INTERVAL = DYNAMIC.TERMINAL_HEARTBEAT_INTERVAL +TERMINAL_ASSET_LIST_SORT_BY = DYNAMIC.TERMINAL_ASSET_LIST_SORT_BY +TERMINAL_ASSET_LIST_PAGE_SIZE = DYNAMIC.TERMINAL_ASSET_LIST_PAGE_SIZE +TERMINAL_SESSION_KEEP_DURATION = DYNAMIC.TERMINAL_SESSION_KEEP_DURATION +TERMINAL_HOST_KEY = DYNAMIC.TERMINAL_HOST_KEY +TERMINAL_HEADER_TITLE = DYNAMIC.TERMINAL_HEADER_TITLE +TERMINAL_TELNET_REGEX = DYNAMIC.TERMINAL_TELNET_REGEX + +# User or user group permission cache time, default 3600 seconds +ASSETS_PERM_CACHE_ENABLE = CONFIG.ASSETS_PERM_CACHE_ENABLE +ASSETS_PERM_CACHE_TIME = CONFIG.ASSETS_PERM_CACHE_TIME + +# Asset user auth external backend, default AuthBook backend +BACKEND_ASSET_USER_AUTH_VAULT = False + +DEFAULT_ORG_SHOW_ALL_USERS = CONFIG.DEFAULT_ORG_SHOW_ALL_USERS +PERM_SINGLE_ASSET_TO_UNGROUP_NODE = CONFIG.PERM_SINGLE_ASSET_TO_UNGROUP_NODE +WINDOWS_SSH_DEFAULT_SHELL = CONFIG.WINDOWS_SSH_DEFAULT_SHELL +FLOWER_URL = CONFIG.FLOWER_URL + +# Enable internal period task +PERIOD_TASK_ENABLED = CONFIG.PERIOD_TASK_ENABLED + +# Email custom content +EMAIL_SUBJECT_PREFIX = DYNAMIC.EMAIL_SUBJECT_PREFIX +EMAIL_SUFFIX = DYNAMIC.EMAIL_SUFFIX +EMAIL_CUSTOM_USER_CREATED_SUBJECT = DYNAMIC.EMAIL_CUSTOM_USER_CREATED_SUBJECT +EMAIL_CUSTOM_USER_CREATED_HONORIFIC = DYNAMIC.EMAIL_CUSTOM_USER_CREATED_HONORIFIC +EMAIL_CUSTOM_USER_CREATED_BODY = DYNAMIC.EMAIL_CUSTOM_USER_CREATED_BODY +EMAIL_CUSTOM_USER_CREATED_SIGNATURE = DYNAMIC.EMAIL_CUSTOM_USER_CREATED_SIGNATURE + +DISPLAY_PER_PAGE = CONFIG.DISPLAY_PER_PAGE +DEFAULT_EXPIRED_YEARS = 70 +USER_GUIDE_URL = DYNAMIC.USER_GUIDE_URL +HTTP_LISTEN_PORT = CONFIG.HTTP_LISTEN_PORT +WS_LISTEN_PORT = CONFIG.WS_LISTEN_PORT +LOGIN_LOG_KEEP_DAYS = DYNAMIC.LOGIN_LOG_KEEP_DAYS diff --git a/jumpserver/jumpserver/apps/jumpserver/settings/libs.py b/jumpserver/jumpserver/apps/jumpserver/settings/libs.py new file mode 100644 index 0000000000000000000000000000000000000000..69b580d667779f96e3de10a8813650544ed799f2 --- /dev/null +++ b/jumpserver/jumpserver/apps/jumpserver/settings/libs.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +# +import os +from ..const import CONFIG, PROJECT_DIR + +REST_FRAMEWORK = { + # Use Django's standard `django.contrib.auth` permissions, + # or allow read-only access for unauthenticated users. + 'DEFAULT_PERMISSION_CLASSES': ( + 'common.permissions.IsOrgAdmin', + ), + 'DEFAULT_RENDERER_CLASSES': ( + 'rest_framework.renderers.JSONRenderer', + 'rest_framework.renderers.BrowsableAPIRenderer', + 'common.drf.renders.JMSCSVRender', + ), + 'DEFAULT_PARSER_CLASSES': ( + 'rest_framework.parsers.JSONParser', + 'rest_framework.parsers.FormParser', + 'rest_framework.parsers.MultiPartParser', + 'common.drf.parsers.JMSCSVParser', + 'rest_framework.parsers.FileUploadParser', + ), + 'DEFAULT_AUTHENTICATION_CLASSES': ( + # 'rest_framework.authentication.BasicAuthentication', + 'authentication.backends.api.AccessKeyAuthentication', + 'authentication.backends.api.AccessTokenAuthentication', + 'authentication.backends.api.PrivateTokenAuthentication', + 'authentication.backends.api.SignatureAuthentication', + 'authentication.backends.api.SessionAuthentication', + ), + 'DEFAULT_FILTER_BACKENDS': ( + 'django_filters.rest_framework.DjangoFilterBackend', + 'rest_framework.filters.SearchFilter', + 'rest_framework.filters.OrderingFilter', + ), + 'DEFAULT_METADATA_CLASS': 'common.drf.metadata.SimpleMetadataWithFilters', + 'ORDERING_PARAM': "order", + 'SEARCH_PARAM': "search", + 'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S %z', + 'DATETIME_INPUT_FORMATS': ['iso-8601', '%Y-%m-%d %H:%M:%S %z'], + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', + # 'PAGE_SIZE': 15 +} + +SWAGGER_SETTINGS = { + 'DEFAULT_AUTO_SCHEMA_CLASS': 'jumpserver.views.swagger.CustomSwaggerAutoSchema', + 'USE_SESSION_AUTH': True, + 'SECURITY_DEFINITIONS': { + 'Bearer': { + 'type': 'apiKey', + 'name': 'Authorization', + 'in': 'header' + } + }, +} + + +# Captcha settings, more see https://django-simple-captcha.readthedocs.io/en/latest/advanced.html +CAPTCHA_IMAGE_SIZE = (80, 33) +CAPTCHA_FOREGROUND_COLOR = '#001100' +CAPTCHA_NOISE_FUNCTIONS = ('captcha.helpers.noise_dots',) +CAPTCHA_TEST_MODE = CONFIG.CAPTCHA_TEST_MODE + +# Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html +BOOTSTRAP3 = { + 'horizontal_label_class': 'col-md-2', + # Field class to use in horizontal forms + 'horizontal_field_class': 'col-md-9', + # Set placeholder attributes to label if no placeholder is provided + 'set_placeholder': False, + 'success_css_class': '', + 'required_css_class': 'required', +} + + +# Django channels support websocket +CHANNEL_REDIS = "redis://:{}@{}:{}/{}".format( + CONFIG.REDIS_PASSWORD, CONFIG.REDIS_HOST, CONFIG.REDIS_PORT, + CONFIG.REDIS_DB_WS, +) + +CHANNEL_LAYERS = { + 'default': { + 'BACKEND': 'channels_redis.core.RedisChannelLayer', + 'CONFIG': { + "hosts": [CHANNEL_REDIS], + }, + }, +} +ASGI_APPLICATION = 'jumpserver.routing.application' + + +# Dump all celery log to here +CELERY_LOG_DIR = os.path.join(PROJECT_DIR, 'data', 'celery') + +# Celery using redis as broker +CELERY_BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % { + 'password': CONFIG.REDIS_PASSWORD, + 'host': CONFIG.REDIS_HOST, + 'port': CONFIG.REDIS_PORT, + 'db': CONFIG.REDIS_DB_CELERY, +} +CELERY_TASK_SERIALIZER = 'pickle' +CELERY_RESULT_SERIALIZER = 'pickle' +CELERY_RESULT_BACKEND = CELERY_BROKER_URL +CELERY_ACCEPT_CONTENT = ['json', 'pickle'] +CELERY_RESULT_EXPIRES = 600 +# CELERY_WORKER_LOG_FORMAT = '%(asctime)s [%(module)s %(levelname)s] %(message)s' +# CELERY_WORKER_LOG_FORMAT = '%(message)s' +# CELERY_WORKER_TASK_LOG_FORMAT = '%(task_id)s %(task_name)s %(message)s' +CELERY_WORKER_TASK_LOG_FORMAT = '%(message)s' +# CELERY_WORKER_LOG_FORMAT = '%(asctime)s [%(module)s %(levelname)s] %(message)s' +CELERY_WORKER_LOG_FORMAT = '%(message)s' +CELERY_TASK_EAGER_PROPAGATES = True +CELERY_WORKER_REDIRECT_STDOUTS = True +CELERY_WORKER_REDIRECT_STDOUTS_LEVEL = "INFO" +# CELERY_WORKER_HIJACK_ROOT_LOGGER = True +# CELERY_WORKER_MAX_TASKS_PER_CHILD = 40 +CELERY_TASK_SOFT_TIME_LIMIT = 3600 diff --git a/jumpserver/jumpserver/apps/jumpserver/settings/logging.py b/jumpserver/jumpserver/apps/jumpserver/settings/logging.py new file mode 100644 index 0000000000000000000000000000000000000000..7de84ab7e56f741bced41502327f46792ec59f14 --- /dev/null +++ b/jumpserver/jumpserver/apps/jumpserver/settings/logging.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +# +import os +from ..const import PROJECT_DIR, CONFIG + +LOG_DIR = os.path.join(PROJECT_DIR, 'logs') +JUMPSERVER_LOG_FILE = os.path.join(LOG_DIR, 'jumpserver.log') +ANSIBLE_LOG_FILE = os.path.join(LOG_DIR, 'ansible.log') +GUNICORN_LOG_FILE = os.path.join(LOG_DIR, 'gunicorn.log') +LOG_LEVEL = CONFIG.LOG_LEVEL + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' + }, + 'main': { + 'datefmt': '%Y-%m-%d %H:%M:%S', + 'format': '%(asctime)s [%(module)s %(levelname)s] %(message)s', + }, + 'simple': { + 'format': '%(levelname)s %(message)s' + }, + 'syslog': { + 'format': 'jumpserver: %(message)s' + }, + 'msg': { + 'format': '%(message)s' + } + }, + 'handlers': { + 'null': { + 'level': 'DEBUG', + 'class': 'logging.NullHandler', + }, + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'main' + }, + 'file': { + 'encoding': 'utf8', + 'level': 'DEBUG', + 'class': 'logging.handlers.RotatingFileHandler', + 'maxBytes': 1024*1024*100, + 'backupCount': 7, + 'formatter': 'main', + 'filename': JUMPSERVER_LOG_FILE, + }, + 'ansible_logs': { + 'encoding': 'utf8', + 'level': 'DEBUG', + 'class': 'logging.handlers.RotatingFileHandler', + 'formatter': 'main', + 'maxBytes': 1024*1024*100, + 'backupCount': 7, + 'filename': ANSIBLE_LOG_FILE, + }, + 'syslog': { + 'level': 'INFO', + 'class': 'logging.NullHandler', + 'formatter': 'syslog' + }, + }, + 'loggers': { + 'django': { + 'handlers': ['null'], + 'propagate': False, + 'level': LOG_LEVEL, + }, + 'django.request': { + 'handlers': ['console', 'file', 'syslog'], + 'level': LOG_LEVEL, + 'propagate': False, + }, + 'django.server': { + 'handlers': ['console', 'file', 'syslog'], + 'level': LOG_LEVEL, + 'propagate': False, + }, + 'jumpserver': { + 'handlers': ['console', 'file'], + 'level': LOG_LEVEL, + }, + 'ops.ansible_api': { + 'handlers': ['console', 'ansible_logs'], + 'level': LOG_LEVEL, + }, + 'django_auth_ldap': { + 'handlers': ['console', 'file'], + 'level': "INFO", + }, + 'syslog': { + 'handlers': ['syslog'], + 'level': 'INFO' + }, + # 'django.db': { + # 'handlers': ['console', 'file'], + # 'level': 'DEBUG' + # } + } +} +SYSLOG_ENABLE = CONFIG.SYSLOG_ENABLE + +if CONFIG.SYSLOG_ADDR != '' and len(CONFIG.SYSLOG_ADDR.split(':')) == 2: + host, port = CONFIG.SYSLOG_ADDR.split(':') + LOGGING['handlers']['syslog'].update({ + 'class': 'logging.handlers.SysLogHandler', + 'facility': CONFIG.SYSLOG_FACILITY, + 'address': (host, int(port)), + 'socktype': CONFIG.SYSLOG_SOCKTYPE, + }) + +if not os.path.isdir(LOG_DIR): + os.makedirs(LOG_DIR) diff --git a/jumpserver/jumpserver/apps/jumpserver/urls.py b/jumpserver/jumpserver/apps/jumpserver/urls.py index 82773a51ab21ff92d130261906d87a1aba65e148..c4653969bbe4a660ae46c894d8c6ae14ffec9044 100644 --- a/jumpserver/jumpserver/apps/jumpserver/urls.py +++ b/jumpserver/jumpserver/apps/jumpserver/urls.py @@ -7,28 +7,26 @@ from django.conf.urls.static import static from django.conf.urls.i18n import i18n_patterns from django.views.i18n import JavaScriptCatalog -# from .views import IndexView, LunaView, I18NView, HealthCheckView, redirect_format_api from . import views -from .celery_flower import celery_flower_view -from .swagger import get_swagger_view api_v1 = [ - path('users/', include('users.urls.api_urls', namespace='api-users')), - path('assets/', include('assets.urls.api_urls', namespace='api-assets')), - path('perms/', include('perms.urls.api_urls', namespace='api-perms')), - path('terminal/', include('terminal.urls.api_urls', namespace='api-terminal')), - path('ops/', include('ops.urls.api_urls', namespace='api-ops')), - path('audits/', include('audits.urls.api_urls', namespace='api-audits')), - path('orgs/', include('orgs.urls.api_urls', namespace='api-orgs')), - path('settings/', include('settings.urls.api_urls', namespace='api-settings')), - path('authentication/', include('authentication.urls.api_urls', namespace='api-auth')), - path('common/', include('common.urls.api_urls', namespace='api-common')), - path('applications/', include('applications.urls.api_urls', namespace='api-applications')), + path('users/', include('users.urls.api_urls', namespace='api-users')), + path('assets/', include('assets.urls.api_urls', namespace='api-assets')), + path('perms/', include('perms.urls.api_urls', namespace='api-perms')), + path('terminal/', include('terminal.urls.api_urls', namespace='api-terminal')), + path('ops/', include('ops.urls.api_urls', namespace='api-ops')), + path('audits/', include('audits.urls.api_urls', namespace='api-audits')), + path('orgs/', include('orgs.urls.api_urls', namespace='api-orgs')), + path('settings/', include('settings.urls.api_urls', namespace='api-settings')), + path('authentication/', include('authentication.urls.api_urls', namespace='api-auth')), + path('common/', include('common.urls.api_urls', namespace='api-common')), + path('applications/', include('applications.urls.api_urls', namespace='api-applications')), + path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')), ] api_v2 = [ - path('terminal/', include('terminal.urls.api_urls_v2', namespace='api-terminal-v2')), - path('users/', include('users.urls.api_urls_v2', namespace='api-users-v2')), + path('terminal/', include('terminal.urls.api_urls_v2', namespace='api-terminal-v2')), + path('users/', include('users.urls.api_urls_v2', namespace='api-users-v2')), ] @@ -42,7 +40,8 @@ app_view_patterns = [ path('orgs/', include('orgs.urls.views_urls', namespace='orgs')), path('auth/', include('authentication.urls.view_urls'), name='auth'), path('applications/', include('applications.urls.views_urls', namespace='applications')), - re_path(r'flower/(?P.*)', celery_flower_view, name='flower-view'), + path('tickets/', include('tickets.urls.views_urls', namespace='tickets')), + re_path(r'flower/(?P.*)', views.celery_flower_view, name='flower-view'), ] @@ -65,7 +64,8 @@ urlpatterns = [ path('api/v2/', include(api_v2)), re_path('api/(?P\w+)/(?Pv\d)/.*', views.redirect_format_api), path('api/health/', views.HealthCheckView.as_view(), name="health"), - path('luna/', views.LunaView.as_view(), name='luna-view'), + re_path('luna/.*', views.LunaView.as_view(), name='luna-view'), + re_path('koko/.*', views.KokoView.as_view(), name='koko-view'), re_path('ws/.*', views.WsView.as_view(), name='ws-view'), path('i18n//', views.I18NView.as_view(), name='i18n-switch'), path('settings/', include('settings.urls.view_urls', namespace='settings')), @@ -79,19 +79,19 @@ urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += js_i18n_patterns -handler404 = 'jumpserver.error_views.handler404' -handler500 = 'jumpserver.error_views.handler500' +handler404 = 'jumpserver.views.handler404' +handler500 = 'jumpserver.views.handler500' if settings.DEBUG: urlpatterns += [ re_path('^swagger(?P\.json|\.yaml)$', - get_swagger_view().without_ui(cache_timeout=1), name='schema-json'), - path('docs/', get_swagger_view().with_ui('swagger', cache_timeout=1), name="docs"), - path('redoc/', get_swagger_view().with_ui('redoc', cache_timeout=1), name='redoc'), + views.get_swagger_view().without_ui(cache_timeout=1), name='schema-json'), + path('docs/', views.get_swagger_view().with_ui('swagger', cache_timeout=1), name="docs"), + path('redoc/', views.get_swagger_view().with_ui('redoc', cache_timeout=1), name='redoc'), re_path('^v2/swagger(?P\.json|\.yaml)$', - get_swagger_view().without_ui(cache_timeout=1), name='schema-json'), - path('docs/v2/', get_swagger_view("v2").with_ui('swagger', cache_timeout=1), name="docs"), - path('redoc/v2/', get_swagger_view("v2").with_ui('redoc', cache_timeout=1), name='redoc'), + views.get_swagger_view().without_ui(cache_timeout=1), name='schema-json'), + path('docs/v2/', views.get_swagger_view("v2").with_ui('swagger', cache_timeout=1), name="docs"), + path('redoc/v2/', views.get_swagger_view("v2").with_ui('redoc', cache_timeout=1), name='redoc'), ] diff --git a/jumpserver/jumpserver/apps/jumpserver/views/__init__.py b/jumpserver/jumpserver/apps/jumpserver/views/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c30537520694da8adba7fa493b8c0928c58616b2 --- /dev/null +++ b/jumpserver/jumpserver/apps/jumpserver/views/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# +from .index import * +from .other import * +from .celery_flower import * +from .swagger import * +from .error_views import * diff --git a/jumpserver/jumpserver/apps/jumpserver/celery_flower.py b/jumpserver/jumpserver/apps/jumpserver/views/celery_flower.py similarity index 95% rename from jumpserver/jumpserver/apps/jumpserver/celery_flower.py rename to jumpserver/jumpserver/apps/jumpserver/views/celery_flower.py index 480745ada3da402921a7ea2c8fd77e60504f0d08..befd3c5f091c60fb593e899b71dd0008cdbb062d 100644 --- a/jumpserver/jumpserver/apps/jumpserver/celery_flower.py +++ b/jumpserver/jumpserver/apps/jumpserver/views/celery_flower.py @@ -9,6 +9,8 @@ from proxy.views import proxy_view flower_url = settings.FLOWER_URL +__all__ = ['celery_flower_view'] + @csrf_exempt def celery_flower_view(request, path): diff --git a/jumpserver/jumpserver/apps/jumpserver/error_views.py b/jumpserver/jumpserver/apps/jumpserver/views/error_views.py similarity index 94% rename from jumpserver/jumpserver/apps/jumpserver/error_views.py rename to jumpserver/jumpserver/apps/jumpserver/views/error_views.py index 0d154d52b4aa612ea25f33886d641a20963364d0..833da9e427382bdf68da48b1ae6eea7d1cf5a34c 100644 --- a/jumpserver/jumpserver/apps/jumpserver/error_views.py +++ b/jumpserver/jumpserver/apps/jumpserver/views/error_views.py @@ -3,6 +3,8 @@ from django.shortcuts import render from django.http import JsonResponse +__all__ = ['handler404', 'handler500'] + def handler404(request, *args, **argv): if request.content_type.find('application/json') > -1: diff --git a/jumpserver/jumpserver/apps/jumpserver/views.py b/jumpserver/jumpserver/apps/jumpserver/views/index.py similarity index 78% rename from jumpserver/jumpserver/apps/jumpserver/views.py rename to jumpserver/jumpserver/apps/jumpserver/views/index.py index f8c8c1f02bdc5132fd248395cb73f9eb8544d11b..2a3304a8c181654d52952969e26c9d3aae2a4312 100644 --- a/jumpserver/jumpserver/apps/jumpserver/views.py +++ b/jumpserver/jumpserver/apps/jumpserver/views/index.py @@ -1,17 +1,10 @@ import datetime -import re -import time -from django.http import HttpResponseRedirect, JsonResponse -from django.conf import settings -from django.views.generic import TemplateView, View +from django.views.generic import TemplateView from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from django.db.models import Count from django.shortcuts import redirect -from rest_framework.views import APIView -from django.views.decorators.csrf import csrf_exempt -from django.http import HttpResponse from users.models import User @@ -19,7 +12,8 @@ from assets.models import Asset from terminal.models import Session from orgs.utils import current_org from common.permissions import PermissionsMixin, IsValidUser -from common.http import HttpResponseTemporaryRedirect + +__all__ = ['IndexView'] class IndexView(PermissionsMixin, TemplateView): @@ -186,51 +180,3 @@ class IndexView(PermissionsMixin, TemplateView): kwargs.update(context) return super(IndexView, self).get_context_data(**kwargs) - - -class LunaView(View): - def get(self, request): - msg = _("
    Luna is a separately deployed program, you need to deploy Luna, koko, configure nginx for url distribution,
    " - "
    If you see this page, prove that you are not accessing the nginx listening port. Good luck.
    ") - return HttpResponse(msg) - - -class I18NView(View): - def get(self, request, lang): - referer_url = request.META.get('HTTP_REFERER', '/') - response = HttpResponseRedirect(referer_url) - response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang) - return response - - -api_url_pattern = re.compile(r'^/api/(?P\w+)/(?Pv\d)/(?P.*)$') - - -@csrf_exempt -def redirect_format_api(request, *args, **kwargs): - _path, query = request.path, request.GET.urlencode() - matched = api_url_pattern.match(_path) - if matched: - kwargs = matched.groupdict() - kwargs["query"] = query - _path = '/api/{version}/{app}/{extra}?{query}'.format(**kwargs).rstrip("?") - return HttpResponseTemporaryRedirect(_path) - else: - return JsonResponse({"msg": "Redirect url failed: {}".format(_path)}, status=404) - - -class HealthCheckView(APIView): - permission_classes = () - - def get(self, request): - return JsonResponse({"status": 1, "time": int(time.time())}) - - -class WsView(APIView): - ws_port = settings.CONFIG.HTTP_LISTEN_PORT + 1 - - def get(self, request): - msg = _("Websocket server run on port: {}, you should proxy it on nginx" - .format(self.ws_port)) - return JsonResponse({"msg": msg}) - diff --git a/jumpserver/jumpserver/apps/jumpserver/views/other.py b/jumpserver/jumpserver/apps/jumpserver/views/other.py new file mode 100644 index 0000000000000000000000000000000000000000..a1db94e777518c3acd265be9865a4d3bf2c39ea0 --- /dev/null +++ b/jumpserver/jumpserver/apps/jumpserver/views/other.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# +import re +import time + +from django.http import HttpResponseRedirect, JsonResponse +from django.conf import settings +from django.views.generic import View +from django.utils.translation import ugettext_lazy as _ +from rest_framework.views import APIView +from django.views.decorators.csrf import csrf_exempt +from django.http import HttpResponse + +from common.http import HttpResponseTemporaryRedirect + + +__all__ = [ + 'LunaView', 'I18NView', 'KokoView', 'WsView', 'HealthCheckView', + 'redirect_format_api' +] + + +class LunaView(View): + def get(self, request): + msg = _("
    Luna is a separately deployed program, you need to deploy Luna, koko, configure nginx for url distribution,
    " + "
    If you see this page, prove that you are not accessing the nginx listening port. Good luck.
    ") + return HttpResponse(msg) + + +class I18NView(View): + def get(self, request, lang): + referer_url = request.META.get('HTTP_REFERER', '/') + response = HttpResponseRedirect(referer_url) + response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang) + return response + + +api_url_pattern = re.compile(r'^/api/(?P\w+)/(?Pv\d)/(?P.*)$') + + +@csrf_exempt +def redirect_format_api(request, *args, **kwargs): + _path, query = request.path, request.GET.urlencode() + matched = api_url_pattern.match(_path) + if matched: + kwargs = matched.groupdict() + kwargs["query"] = query + _path = '/api/{version}/{app}/{extra}?{query}'.format(**kwargs).rstrip("?") + return HttpResponseTemporaryRedirect(_path) + else: + return JsonResponse({"msg": "Redirect url failed: {}".format(_path)}, status=404) + + +class HealthCheckView(APIView): + permission_classes = () + + def get(self, request): + return JsonResponse({"status": 1, "time": int(time.time())}) + + +class WsView(APIView): + ws_port = settings.HTTP_LISTEN_PORT + 1 + + def get(self, request): + msg = _("Websocket server run on port: {}, you should proxy it on nginx" + .format(self.ws_port)) + return JsonResponse({"msg": msg}) + + +class KokoView(View): + def get(self, request): + msg = _( + "
    Koko is a separately deployed program, you need to deploy Koko, configure nginx for url distribution,
    " + "
    If you see this page, prove that you are not accessing the nginx listening port. Good luck.
    ") + return HttpResponse(msg) diff --git a/jumpserver/jumpserver/apps/jumpserver/swagger.py b/jumpserver/jumpserver/apps/jumpserver/views/swagger.py similarity index 98% rename from jumpserver/jumpserver/apps/jumpserver/swagger.py rename to jumpserver/jumpserver/apps/jumpserver/views/swagger.py index b68733b637452889d809089129bf4e227f1fbb8c..4aed12663f4d4a2311bd22561dead863d7bb8cb0 100644 --- a/jumpserver/jumpserver/apps/jumpserver/swagger.py +++ b/jumpserver/jumpserver/apps/jumpserver/views/swagger.py @@ -50,7 +50,7 @@ class CustomSwaggerAutoSchema(SwaggerAutoSchema): def get_swagger_view(version='v1'): - from .urls import api_v1, api_v2 + from ..urls import api_v1, api_v2 from django.urls import path, include api_v1_patterns = [ path('api/v1/', include(api_v1)) diff --git a/jumpserver/jumpserver/apps/locale/zh/LC_MESSAGES/django.mo b/jumpserver/jumpserver/apps/locale/zh/LC_MESSAGES/django.mo index 233d13246a019e16a6731f51c2b8631aecb551a3..89354bcf1d09cbd9cb194210c3fb13c358df6e2a 100644 Binary files a/jumpserver/jumpserver/apps/locale/zh/LC_MESSAGES/django.mo and b/jumpserver/jumpserver/apps/locale/zh/LC_MESSAGES/django.mo differ diff --git a/jumpserver/jumpserver/apps/locale/zh/LC_MESSAGES/django.po b/jumpserver/jumpserver/apps/locale/zh/LC_MESSAGES/django.po index 95f544397f2b4600790099813a290692b85c2d6e..867ddeb7822529a7d7d9a6f8fbafee4b0120ee7d 100644 --- a/jumpserver/jumpserver/apps/locale/zh/LC_MESSAGES/django.po +++ b/jumpserver/jumpserver/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Jumpserver 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-11-13 16:38+0800\n" +"POT-Creation-Date: 2020-01-15 12:40+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: Jumpserver team\n" @@ -17,165 +17,285 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: applications/const.py:17 -msgid "Browser" -msgstr "浏览器" - -#: applications/const.py:23 -msgid "Database tools" -msgstr "数据库工具" - -#: applications/const.py:29 -msgid "Virtualization tools" -msgstr "虚拟化工具" - -#: applications/const.py:34 +#: applications/const.py:52 msgid "Custom" msgstr "自定义" -#: applications/forms/remote_app.py:21 +#: applications/forms/remote_app.py:44 applications/models/remote_app.py:23 +#: applications/templates/applications/remote_app_detail.html:52 +#: applications/templates/applications/remote_app_list.html:27 +#: applications/templates/applications/user_remote_app_list.html:18 +#: assets/forms/domain.py:15 assets/forms/label.py:13 +#: assets/models/asset.py:342 assets/models/authbook.py:24 +#: assets/models/gathered_user.py:14 assets/serializers/admin_user.py:32 +#: assets/serializers/asset_user.py:82 assets/serializers/system_user.py:41 +#: assets/templates/assets/admin_user_list.html:23 +#: assets/templates/assets/asset_list.html:170 +#: assets/templates/assets/domain_detail.html:55 +#: assets/templates/assets/domain_list.html:22 +#: assets/templates/assets/label_list.html:16 +#: assets/templates/assets/system_user_list.html:28 audits/models.py:20 +#: audits/templates/audits/ftp_log_list.html:45 +#: audits/templates/audits/ftp_log_list.html:75 +#: perms/forms/asset_permission.py:89 perms/models/asset_permission.py:80 +#: perms/templates/perms/asset_permission_asset.html:53 +#: perms/templates/perms/asset_permission_create_update.html:57 +#: perms/templates/perms/asset_permission_list.html:35 +#: perms/templates/perms/asset_permission_list.html:87 +#: terminal/backends/command/models.py:13 terminal/models.py:178 +#: terminal/templates/terminal/command_list.html:30 +#: terminal/templates/terminal/command_list.html:66 +#: terminal/templates/terminal/session_list.html:26 +#: terminal/templates/terminal/session_list.html:70 +#: users/templates/users/user_asset_permission.html:40 +#: users/templates/users/user_asset_permission.html:70 +#: users/templates/users/user_granted_remote_app.html:36 +#: xpack/plugins/change_auth_plan/forms.py:73 +#: xpack/plugins/change_auth_plan/models.py:418 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:40 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:54 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:13 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:14 +#: xpack/plugins/cloud/models.py:306 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:60 +#: xpack/plugins/orgs/templates/orgs/org_list.html:17 +#: xpack/plugins/vault/forms.py:13 xpack/plugins/vault/forms.py:15 +msgid "Asset" +msgstr "资产" + +#: applications/forms/remote_app.py:55 msgid "Target URL" msgstr "目标URL" -#: applications/forms/remote_app.py:24 applications/forms/remote_app.py:53 -#: applications/forms/remote_app.py:69 +#: applications/forms/remote_app.py:58 applications/forms/remote_app.py:97 +#: applications/forms/remote_app.py:114 msgid "Login username" msgstr "登录账号" -#: applications/forms/remote_app.py:28 applications/forms/remote_app.py:57 -#: applications/forms/remote_app.py:73 +#: applications/forms/remote_app.py:62 applications/forms/remote_app.py:101 +#: applications/forms/remote_app.py:118 msgid "Login password" msgstr "登录密码" -#: applications/forms/remote_app.py:34 +#: applications/forms/remote_app.py:73 msgid "Database IP" msgstr "数据库IP" -#: applications/forms/remote_app.py:37 +#: applications/forms/remote_app.py:76 msgid "Database name" msgstr "数据库名" -#: applications/forms/remote_app.py:40 +#: applications/forms/remote_app.py:79 msgid "Database username" msgstr "数据库账号" -#: applications/forms/remote_app.py:44 +#: applications/forms/remote_app.py:83 msgid "Database password" msgstr "数据库密码" -#: applications/forms/remote_app.py:50 applications/forms/remote_app.py:66 +#: applications/forms/remote_app.py:94 applications/forms/remote_app.py:111 msgid "Target address" msgstr "目标地址" -#: applications/forms/remote_app.py:63 +#: applications/forms/remote_app.py:108 msgid "Operating parameter" msgstr "运行参数" -#: applications/forms/remote_app.py:100 applications/models/remote_app.py:23 -#: applications/templates/applications/remote_app_detail.html:57 -#: applications/templates/applications/remote_app_list.html:22 -#: applications/templates/applications/user_remote_app_list.html:18 -#: assets/forms/domain.py:15 assets/forms/label.py:13 -#: assets/models/asset.py:295 assets/models/authbook.py:24 -#: assets/models/gathered_user.py:14 assets/serializers/admin_user.py:32 -#: assets/serializers/asset_user.py:82 assets/serializers/system_user.py:37 -#: assets/templates/assets/admin_user_list.html:46 -#: assets/templates/assets/domain_detail.html:60 -#: assets/templates/assets/domain_list.html:26 -#: assets/templates/assets/label_list.html:16 -#: assets/templates/assets/system_user_list.html:51 audits/models.py:20 -#: audits/templates/audits/ftp_log_list.html:44 -#: audits/templates/audits/ftp_log_list.html:74 -#: perms/forms/asset_permission.py:84 perms/models/asset_permission.py:80 -#: perms/templates/perms/asset_permission_create_update.html:45 -#: perms/templates/perms/asset_permission_list.html:52 -#: perms/templates/perms/asset_permission_list.html:121 -#: terminal/backends/command/models.py:13 terminal/models.py:157 -#: terminal/templates/terminal/command_list.html:30 -#: terminal/templates/terminal/command_list.html:66 -#: terminal/templates/terminal/session_list.html:28 -#: terminal/templates/terminal/session_list.html:72 -#: xpack/plugins/change_auth_plan/forms.py:73 -#: xpack/plugins/change_auth_plan/models.py:412 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:46 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:54 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:13 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:14 -#: xpack/plugins/cloud/models.py:307 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:63 -#: xpack/plugins/orgs/templates/orgs/org_list.html:17 -#: xpack/plugins/vault/forms.py:13 xpack/plugins/vault/forms.py:15 -msgid "Asset" -msgstr "资产" - -#: applications/models/remote_app.py:21 -#: applications/templates/applications/remote_app_detail.html:53 -#: applications/templates/applications/remote_app_list.html:20 +#: applications/models/database_app.py:18 applications/models/remote_app.py:21 +#: applications/templates/applications/database_app_detail.html:48 +#: applications/templates/applications/database_app_list.html:23 +#: applications/templates/applications/remote_app_detail.html:48 +#: applications/templates/applications/remote_app_list.html:25 +#: applications/templates/applications/user_database_app_list.html:16 #: applications/templates/applications/user_remote_app_list.html:16 -#: assets/forms/asset.py:21 assets/forms/domain.py:77 assets/forms/user.py:75 -#: assets/forms/user.py:95 assets/models/base.py:28 assets/models/cluster.py:18 -#: assets/models/cmd_filter.py:21 assets/models/domain.py:20 -#: assets/models/group.py:20 assets/models/label.py:18 -#: assets/templates/assets/admin_user_detail.html:56 -#: assets/templates/assets/admin_user_list.html:44 -#: assets/templates/assets/cmd_filter_detail.html:61 -#: assets/templates/assets/cmd_filter_list.html:24 -#: assets/templates/assets/domain_detail.html:56 -#: assets/templates/assets/domain_gateway_list.html:67 -#: assets/templates/assets/domain_list.html:25 +#: assets/forms/asset.py:20 assets/forms/domain.py:77 assets/forms/user.py:74 +#: assets/forms/user.py:94 assets/models/asset.py:144 assets/models/base.py:28 +#: assets/models/cluster.py:18 assets/models/cmd_filter.py:21 +#: assets/models/domain.py:20 assets/models/group.py:20 +#: assets/models/label.py:18 assets/templates/assets/_node_detail_modal.html:27 +#: assets/templates/assets/admin_user_detail.html:51 +#: assets/templates/assets/admin_user_list.html:21 +#: assets/templates/assets/cmd_filter_detail.html:56 +#: assets/templates/assets/cmd_filter_list.html:22 +#: assets/templates/assets/domain_detail.html:51 +#: assets/templates/assets/domain_gateway_list.html:62 +#: assets/templates/assets/domain_list.html:21 #: assets/templates/assets/label_list.html:14 -#: assets/templates/assets/system_user_detail.html:58 -#: assets/templates/assets/system_user_list.html:47 ops/models/adhoc.py:37 -#: ops/templates/ops/task_detail.html:60 ops/templates/ops/task_list.html:11 +#: assets/templates/assets/platform_detail.html:43 +#: assets/templates/assets/platform_list.html:16 +#: assets/templates/assets/system_user_detail.html:55 +#: assets/templates/assets/system_user_list.html:24 ops/models/adhoc.py:40 +#: ops/templates/ops/task_detail.html:58 ops/templates/ops/task_list.html:11 #: orgs/models.py:12 perms/models/base.py:48 -#: perms/templates/perms/asset_permission_detail.html:62 -#: perms/templates/perms/asset_permission_list.html:49 -#: perms/templates/perms/asset_permission_list.html:68 -#: perms/templates/perms/asset_permission_user.html:54 -#: perms/templates/perms/remote_app_permission_detail.html:62 +#: perms/templates/perms/asset_permission_detail.html:57 +#: perms/templates/perms/asset_permission_list.html:32 +#: perms/templates/perms/asset_permission_list.html:183 +#: perms/templates/perms/asset_permission_user.html:53 +#: perms/templates/perms/database_app_permission_detail.html:57 +#: perms/templates/perms/database_app_permission_list.html:14 +#: perms/templates/perms/database_app_permission_user.html:53 +#: perms/templates/perms/remote_app_permission_detail.html:57 #: perms/templates/perms/remote_app_permission_list.html:14 -#: perms/templates/perms/remote_app_permission_remote_app.html:53 -#: perms/templates/perms/remote_app_permission_user.html:53 -#: settings/models.py:29 +#: perms/templates/perms/remote_app_permission_remote_app.html:49 +#: perms/templates/perms/remote_app_permission_user.html:49 +#: settings/models.py:26 #: settings/templates/settings/_ldap_list_users_modal.html:32 -#: settings/templates/settings/command_storage_create.html:41 -#: settings/templates/settings/replay_storage_create.html:44 -#: settings/templates/settings/terminal_setting.html:83 -#: settings/templates/settings/terminal_setting.html:105 terminal/models.py:23 -#: terminal/models.py:260 terminal/templates/terminal/terminal_detail.html:43 -#: terminal/templates/terminal/terminal_list.html:29 users/models/group.py:14 -#: users/models/user.py:373 users/templates/users/_select_user_modal.html:13 -#: users/templates/users/user_detail.html:63 -#: users/templates/users/user_group_detail.html:55 -#: users/templates/users/user_group_list.html:35 -#: users/templates/users/user_list.html:35 +#: terminal/models.py:26 terminal/models.py:282 terminal/models.py:314 +#: terminal/models.py:351 terminal/templates/terminal/base_storage_list.html:31 +#: terminal/templates/terminal/terminal_detail.html:43 +#: terminal/templates/terminal/terminal_list.html:30 users/forms/profile.py:20 +#: users/models/group.py:15 users/models/user.py:438 +#: users/templates/users/_select_user_modal.html:13 +#: users/templates/users/user_asset_permission.html:37 +#: users/templates/users/user_asset_permission.html:154 +#: users/templates/users/user_database_app_permission.html:36 +#: users/templates/users/user_detail.html:49 +#: users/templates/users/user_granted_database_app.html:34 +#: users/templates/users/user_granted_remote_app.html:34 +#: users/templates/users/user_group_detail.html:50 +#: users/templates/users/user_group_list.html:14 +#: users/templates/users/user_list.html:14 #: users/templates/users/user_profile.html:51 #: users/templates/users/user_pubkey_update.html:57 +#: users/templates/users/user_remote_app_permission.html:36 #: xpack/plugins/change_auth_plan/forms.py:56 #: xpack/plugins/change_auth_plan/models.py:63 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:61 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:59 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:12 -#: xpack/plugins/cloud/models.py:59 xpack/plugins/cloud/models.py:144 -#: xpack/plugins/cloud/templates/cloud/account_detail.html:50 +#: xpack/plugins/cloud/models.py:58 xpack/plugins/cloud/models.py:143 +#: xpack/plugins/cloud/templates/cloud/account_detail.html:47 #: xpack/plugins/cloud/templates/cloud/account_list.html:12 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:56 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:53 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:12 #: xpack/plugins/gathered_user/models.py:28 #: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:16 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:52 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:47 #: xpack/plugins/orgs/templates/orgs/org_list.html:12 msgid "Name" msgstr "名称" +#: applications/models/database_app.py:22 +#: applications/templates/applications/database_app_detail.html:52 +#: applications/templates/applications/database_app_list.html:24 +#: applications/templates/applications/user_database_app_list.html:17 +#: assets/models/cmd_filter.py:51 +#: assets/templates/assets/cmd_filter_rule_list.html:53 +#: audits/templates/audits/login_log_list.html:58 +#: perms/templates/perms/remote_app_permission_remote_app.html:50 +#: terminal/models.py:316 terminal/models.py:353 +#: terminal/templates/terminal/base_storage_list.html:32 +#: tickets/models/ticket.py:43 tickets/templates/tickets/ticket_detail.html:33 +#: tickets/templates/tickets/ticket_list.html:35 +#: users/templates/users/user_granted_database_app.html:35 +msgid "Type" +msgstr "类型" + +#: applications/models/database_app.py:25 +#: applications/templates/applications/database_app_detail.html:56 +#: applications/templates/applications/database_app_list.html:25 +#: applications/templates/applications/user_database_app_list.html:18 +#: ops/models/adhoc.py:185 templates/index.html:91 +#: users/templates/users/user_granted_database_app.html:36 +msgid "Host" +msgstr "主机" + +#: applications/models/database_app.py:27 +#: applications/templates/applications/database_app_detail.html:60 +#: applications/templates/applications/database_app_list.html:26 +#: assets/forms/asset.py:24 assets/models/asset.py:190 +#: assets/models/domain.py:50 +#: assets/templates/assets/domain_gateway_list.html:64 +msgid "Port" +msgstr "端口" + +#: applications/models/database_app.py:29 +#: applications/templates/applications/database_app_detail.html:64 +#: applications/templates/applications/database_app_list.html:27 +#: applications/templates/applications/user_database_app_list.html:19 +#: users/templates/users/user_granted_database_app.html:37 +msgid "Database" +msgstr "数据库" + +# msgid "Date created" +# msgstr "创建日期" +#: applications/models/database_app.py:33 applications/models/remote_app.py:45 +#: applications/templates/applications/database_app_detail.html:76 +#: applications/templates/applications/database_app_list.html:28 +#: applications/templates/applications/remote_app_detail.html:72 +#: applications/templates/applications/remote_app_list.html:28 +#: applications/templates/applications/user_database_app_list.html:20 +#: applications/templates/applications/user_remote_app_list.html:19 +#: assets/models/asset.py:149 assets/models/asset.py:225 +#: assets/models/base.py:33 assets/models/cluster.py:29 +#: assets/models/cmd_filter.py:23 assets/models/cmd_filter.py:56 +#: assets/models/domain.py:21 assets/models/domain.py:53 +#: assets/models/group.py:23 assets/models/label.py:23 +#: assets/templates/assets/admin_user_detail.html:67 +#: assets/templates/assets/admin_user_list.html:24 +#: assets/templates/assets/asset_detail.html:128 +#: assets/templates/assets/cmd_filter_detail.html:60 +#: assets/templates/assets/cmd_filter_list.html:25 +#: assets/templates/assets/cmd_filter_rule_list.html:57 +#: assets/templates/assets/domain_detail.html:71 +#: assets/templates/assets/domain_gateway_list.html:67 +#: assets/templates/assets/domain_list.html:24 +#: assets/templates/assets/platform_detail.html:59 +#: assets/templates/assets/platform_list.html:18 +#: assets/templates/assets/system_user_detail.html:101 +#: assets/templates/assets/system_user_list.html:32 ops/models/adhoc.py:46 +#: orgs/models.py:18 perms/models/base.py:56 +#: perms/templates/perms/asset_permission_detail.html:97 +#: perms/templates/perms/database_app_permission_detail.html:93 +#: perms/templates/perms/remote_app_permission_detail.html:89 +#: settings/models.py:31 terminal/models.py:36 terminal/models.py:321 +#: terminal/models.py:358 terminal/templates/terminal/base_storage_list.html:33 +#: terminal/templates/terminal/terminal_detail.html:63 +#: tickets/templates/tickets/ticket_detail.html:104 users/models/group.py:16 +#: users/models/user.py:471 users/templates/users/user_detail.html:115 +#: users/templates/users/user_granted_database_app.html:38 +#: users/templates/users/user_granted_remote_app.html:37 +#: users/templates/users/user_group_detail.html:62 +#: users/templates/users/user_group_list.html:16 +#: users/templates/users/user_profile.html:138 +#: xpack/plugins/change_auth_plan/models.py:104 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:115 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:19 +#: xpack/plugins/cloud/models.py:76 xpack/plugins/cloud/models.py:172 +#: xpack/plugins/cloud/templates/cloud/account_detail.html:67 +#: xpack/plugins/cloud/templates/cloud/account_list.html:15 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:102 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:18 +#: xpack/plugins/gathered_user/models.py:42 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:59 +#: xpack/plugins/orgs/templates/orgs/org_list.html:23 +msgid "Comment" +msgstr "备注" + +#: applications/models/database_app.py:41 +#: perms/forms/database_app_permission.py:44 +#: perms/models/database_app_permission.py:17 +#: perms/templates/perms/database_app_permission_create_update.html:46 +#: perms/templates/perms/database_app_permission_database_app.html:23 +#: perms/templates/perms/database_app_permission_database_app.html:53 +#: perms/templates/perms/database_app_permission_detail.html:22 +#: perms/templates/perms/database_app_permission_list.html:17 +#: perms/templates/perms/database_app_permission_user.html:23 +#: templates/_nav.html:66 templates/_nav.html:86 templates/_nav_user.html:22 +#: users/templates/users/user_database_app_permission.html:39 +#: users/templates/users/user_database_app_permission.html:64 +msgid "DatabaseApp" +msgstr "数据库应用" + #: applications/models/remote_app.py:28 -#: applications/templates/applications/remote_app_detail.html:61 -#: applications/templates/applications/remote_app_list.html:21 +#: applications/templates/applications/remote_app_detail.html:56 +#: applications/templates/applications/remote_app_list.html:26 #: applications/templates/applications/user_remote_app_list.html:17 +#: users/templates/users/user_granted_remote_app.html:35 msgid "App type" msgstr "应用类型" #: applications/models/remote_app.py:32 -#: applications/templates/applications/remote_app_detail.html:65 +#: applications/templates/applications/remote_app_detail.html:60 msgid "App path" msgstr "应用路径" @@ -184,24 +304,26 @@ msgid "Parameters" msgstr "参数" #: applications/models/remote_app.py:39 -#: applications/templates/applications/remote_app_detail.html:73 -#: assets/models/asset.py:174 assets/models/base.py:36 +#: applications/templates/applications/database_app_detail.html:72 +#: applications/templates/applications/remote_app_detail.html:68 +#: assets/models/asset.py:223 assets/models/base.py:36 #: assets/models/cluster.py:28 assets/models/cmd_filter.py:26 #: assets/models/cmd_filter.py:59 assets/models/group.py:21 -#: assets/templates/assets/admin_user_detail.html:68 -#: assets/templates/assets/asset_detail.html:122 -#: assets/templates/assets/cmd_filter_detail.html:77 -#: assets/templates/assets/domain_detail.html:72 -#: assets/templates/assets/system_user_detail.html:100 -#: common/mixins/models.py:50 ops/templates/ops/adhoc_detail.html:86 +#: assets/templates/assets/admin_user_detail.html:63 +#: assets/templates/assets/asset_detail.html:120 +#: assets/templates/assets/cmd_filter_detail.html:72 +#: assets/templates/assets/domain_detail.html:67 +#: assets/templates/assets/system_user_detail.html:97 +#: common/mixins/models.py:50 ops/templates/ops/adhoc_detail.html:84 #: orgs/models.py:16 perms/models/base.py:54 -#: perms/templates/perms/asset_permission_detail.html:98 -#: perms/templates/perms/remote_app_permission_detail.html:90 -#: users/models/user.py:414 users/serializers/group.py:32 -#: users/templates/users/user_detail.html:111 +#: perms/templates/perms/asset_permission_detail.html:93 +#: perms/templates/perms/database_app_permission_detail.html:89 +#: perms/templates/perms/remote_app_permission_detail.html:85 +#: users/models/user.py:479 users/serializers/group.py:32 +#: users/templates/users/user_detail.html:97 #: xpack/plugins/change_auth_plan/models.py:108 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:113 -#: xpack/plugins/cloud/models.py:80 xpack/plugins/cloud/models.py:179 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:111 +#: xpack/plugins/cloud/models.py:79 xpack/plugins/cloud/models.py:178 #: xpack/plugins/gathered_user/models.py:46 msgid "Created by" msgstr "创建者" @@ -209,363 +331,387 @@ msgstr "创建者" # msgid "Created by" # msgstr "创建者" #: applications/models/remote_app.py:42 -#: applications/templates/applications/remote_app_detail.html:69 -#: assets/models/asset.py:175 assets/models/base.py:34 +#: applications/templates/applications/database_app_detail.html:68 +#: applications/templates/applications/remote_app_detail.html:64 +#: assets/models/asset.py:224 assets/models/base.py:34 #: assets/models/cluster.py:26 assets/models/domain.py:23 #: assets/models/gathered_user.py:19 assets/models/group.py:22 -#: assets/models/label.py:25 assets/templates/assets/admin_user_detail.html:64 -#: assets/templates/assets/cmd_filter_detail.html:69 -#: assets/templates/assets/domain_detail.html:68 -#: assets/templates/assets/system_user_detail.html:96 -#: common/mixins/models.py:51 ops/models/adhoc.py:45 -#: ops/templates/ops/adhoc_detail.html:90 ops/templates/ops/task_detail.html:64 +#: assets/models/label.py:25 assets/templates/assets/admin_user_detail.html:59 +#: assets/templates/assets/cmd_filter_detail.html:64 +#: assets/templates/assets/domain_detail.html:63 +#: assets/templates/assets/system_user_detail.html:93 +#: common/mixins/models.py:51 ops/models/adhoc.py:48 +#: ops/templates/ops/adhoc_detail.html:88 ops/templates/ops/task_detail.html:62 #: orgs/models.py:17 perms/models/base.py:55 -#: perms/templates/perms/asset_permission_detail.html:94 -#: perms/templates/perms/remote_app_permission_detail.html:86 -#: terminal/templates/terminal/terminal_detail.html:59 users/models/group.py:17 -#: users/templates/users/user_group_detail.html:63 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:105 -#: xpack/plugins/cloud/models.py:83 xpack/plugins/cloud/models.py:182 -#: xpack/plugins/cloud/templates/cloud/account_detail.html:66 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:101 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:60 +#: perms/templates/perms/asset_permission_detail.html:89 +#: perms/templates/perms/database_app_permission_detail.html:85 +#: perms/templates/perms/remote_app_permission_detail.html:81 +#: terminal/templates/terminal/terminal_detail.html:59 +#: tickets/templates/tickets/ticket_detail.html:52 users/models/group.py:18 +#: users/templates/users/user_group_detail.html:58 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:103 +#: xpack/plugins/cloud/models.py:82 xpack/plugins/cloud/models.py:181 +#: xpack/plugins/cloud/templates/cloud/account_detail.html:63 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:98 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:55 msgid "Date created" msgstr "创建日期" -# msgid "Date created" -# msgstr "创建日期" -#: applications/models/remote_app.py:45 -#: applications/templates/applications/remote_app_detail.html:77 -#: applications/templates/applications/remote_app_list.html:23 -#: applications/templates/applications/user_remote_app_list.html:19 -#: assets/models/asset.py:176 assets/models/base.py:33 -#: assets/models/cluster.py:29 assets/models/cmd_filter.py:23 -#: assets/models/cmd_filter.py:56 assets/models/domain.py:21 -#: assets/models/domain.py:53 assets/models/group.py:23 -#: assets/models/label.py:23 assets/templates/assets/admin_user_detail.html:72 -#: assets/templates/assets/admin_user_list.html:50 -#: assets/templates/assets/asset_detail.html:130 -#: assets/templates/assets/cmd_filter_detail.html:65 -#: assets/templates/assets/cmd_filter_list.html:27 -#: assets/templates/assets/cmd_filter_rule_list.html:62 -#: assets/templates/assets/domain_detail.html:76 -#: assets/templates/assets/domain_gateway_list.html:72 -#: assets/templates/assets/domain_list.html:28 -#: assets/templates/assets/system_user_detail.html:104 -#: assets/templates/assets/system_user_list.html:55 ops/models/adhoc.py:43 -#: orgs/models.py:18 perms/models/base.py:56 -#: perms/templates/perms/asset_permission_detail.html:102 -#: perms/templates/perms/remote_app_permission_detail.html:94 -#: settings/models.py:34 terminal/models.py:33 -#: terminal/templates/terminal/terminal_detail.html:63 users/models/group.py:15 -#: users/models/user.py:406 users/templates/users/user_detail.html:129 -#: users/templates/users/user_group_detail.html:67 -#: users/templates/users/user_group_list.html:37 -#: users/templates/users/user_profile.html:138 -#: xpack/plugins/change_auth_plan/models.py:104 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:117 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:19 -#: xpack/plugins/cloud/models.py:77 xpack/plugins/cloud/models.py:173 -#: xpack/plugins/cloud/templates/cloud/account_detail.html:70 -#: xpack/plugins/cloud/templates/cloud/account_list.html:15 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:105 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:18 -#: xpack/plugins/gathered_user/models.py:42 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:64 -#: xpack/plugins/orgs/templates/orgs/org_list.html:23 -msgid "Comment" -msgstr "备注" - -#: applications/models/remote_app.py:49 perms/forms/remote_app_permission.py:40 +#: applications/models/remote_app.py:49 perms/forms/remote_app_permission.py:46 #: perms/models/remote_app_permission.py:15 -#: perms/templates/perms/remote_app_permission_create_update.html:48 -#: perms/templates/perms/remote_app_permission_detail.html:27 +#: perms/templates/perms/remote_app_permission_create_update.html:46 +#: perms/templates/perms/remote_app_permission_detail.html:22 #: perms/templates/perms/remote_app_permission_list.html:17 -#: perms/templates/perms/remote_app_permission_remote_app.html:26 -#: perms/templates/perms/remote_app_permission_user.html:26 -#: templates/_nav.html:60 templates/_nav.html:76 templates/_nav_user.html:16 +#: perms/templates/perms/remote_app_permission_remote_app.html:22 +#: perms/templates/perms/remote_app_permission_user.html:22 +#: templates/_nav.html:64 templates/_nav.html:82 templates/_nav_user.html:16 +#: users/templates/users/user_remote_app_permission.html:39 +#: users/templates/users/user_remote_app_permission.html:64 msgid "RemoteApp" msgstr "远程应用" -#: applications/templates/applications/remote_app_create_update.html:55 -#: assets/templates/assets/_system_user.html:75 -#: assets/templates/assets/admin_user_create_update.html:45 +#: applications/templates/applications/database_app_create_update.html:12 +#: applications/templates/applications/remote_app_create_update.html:12 +#: assets/templates/assets/_system_user.html:71 +#: assets/templates/assets/admin_user_create_update.html:41 #: assets/templates/assets/asset_bulk_update.html:23 #: assets/templates/assets/asset_create.html:81 #: assets/templates/assets/cmd_filter_create_update.html:15 -#: assets/templates/assets/cmd_filter_rule_create_update.html:40 +#: assets/templates/assets/cmd_filter_rule_create_update.html:36 #: assets/templates/assets/domain_create_update.html:16 -#: assets/templates/assets/gateway_create_update.html:58 +#: assets/templates/assets/gateway_create_update.html:54 #: assets/templates/assets/label_create_update.html:18 -#: perms/templates/perms/asset_permission_create_update.html:83 -#: perms/templates/perms/remote_app_permission_create_update.html:84 -#: settings/templates/settings/basic_setting.html:64 -#: settings/templates/settings/command_storage_create.html:79 -#: settings/templates/settings/email_content_setting.html:54 -#: settings/templates/settings/email_setting.html:65 -#: settings/templates/settings/ldap_setting.html:64 -#: settings/templates/settings/replay_storage_create.html:152 -#: settings/templates/settings/security_setting.html:73 -#: settings/templates/settings/terminal_setting.html:71 -#: terminal/templates/terminal/terminal_update.html:45 +#: assets/templates/assets/platform_create_update.html:20 +#: perms/templates/perms/asset_permission_create_update.html:127 +#: perms/templates/perms/database_app_permission_create_update.html:82 +#: perms/templates/perms/remote_app_permission_create_update.html:82 +#: settings/templates/settings/basic_setting.html:45 +#: settings/templates/settings/email_content_setting.html:35 +#: settings/templates/settings/email_setting.html:46 +#: settings/templates/settings/ldap_setting.html:45 +#: settings/templates/settings/security_setting.html:54 +#: settings/templates/settings/terminal_setting.html:53 +#: terminal/templates/terminal/base_storage_create_update.html:12 +#: terminal/templates/terminal/terminal_update.html:43 #: users/templates/users/_user.html:51 #: users/templates/users/user_bulk_update.html:23 -#: users/templates/users/user_detail.html:178 -#: users/templates/users/user_group_create_update.html:31 +#: users/templates/users/user_detail.html:168 +#: users/templates/users/user_group_create_update.html:27 #: users/templates/users/user_password_update.html:75 #: users/templates/users/user_profile.html:209 #: users/templates/users/user_profile_update.html:67 #: users/templates/users/user_pubkey_update.html:74 #: users/templates/users/user_pubkey_update.html:80 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:71 -#: xpack/plugins/cloud/templates/cloud/account_create_update.html:33 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:53 -#: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:44 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:65 +#: xpack/plugins/cloud/templates/cloud/account_create_update.html:29 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:49 +#: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:40 #: xpack/plugins/interface/templates/interface/interface.html:72 -#: xpack/plugins/orgs/templates/orgs/org_create_update.html:33 -#: xpack/plugins/vault/templates/vault/vault_create.html:45 +#: xpack/plugins/orgs/templates/orgs/org_create_update.html:29 +#: xpack/plugins/vault/templates/vault/vault_create.html:41 msgid "Reset" msgstr "重置" -#: applications/templates/applications/remote_app_create_update.html:57 -#: assets/templates/assets/_system_user.html:76 -#: assets/templates/assets/admin_user_create_update.html:46 +#: applications/templates/applications/database_app_create_update.html:13 +#: applications/templates/applications/remote_app_create_update.html:14 +#: assets/templates/assets/_system_user.html:72 +#: assets/templates/assets/admin_user_create_update.html:42 #: assets/templates/assets/asset_bulk_update.html:24 #: assets/templates/assets/asset_create.html:82 -#: assets/templates/assets/asset_list.html:117 +#: assets/templates/assets/asset_list.html:45 #: assets/templates/assets/cmd_filter_create_update.html:16 -#: assets/templates/assets/cmd_filter_rule_create_update.html:41 +#: assets/templates/assets/cmd_filter_rule_create_update.html:37 #: assets/templates/assets/domain_create_update.html:17 -#: assets/templates/assets/gateway_create_update.html:59 +#: assets/templates/assets/gateway_create_update.html:55 #: assets/templates/assets/label_create_update.html:19 +#: assets/templates/assets/platform_create_update.html:21 #: audits/templates/audits/login_log_list.html:95 -#: perms/templates/perms/asset_permission_create_update.html:84 -#: perms/templates/perms/remote_app_permission_create_update.html:85 -#: settings/templates/settings/basic_setting.html:65 -#: settings/templates/settings/command_storage_create.html:80 -#: settings/templates/settings/email_content_setting.html:55 -#: settings/templates/settings/email_setting.html:66 -#: settings/templates/settings/ldap_setting.html:67 -#: settings/templates/settings/replay_storage_create.html:153 -#: settings/templates/settings/security_setting.html:74 -#: settings/templates/settings/terminal_setting.html:73 +#: perms/templates/perms/asset_permission_create_update.html:128 +#: perms/templates/perms/database_app_permission_create_update.html:83 +#: perms/templates/perms/remote_app_permission_create_update.html:83 +#: settings/templates/settings/basic_setting.html:46 +#: settings/templates/settings/email_content_setting.html:36 +#: settings/templates/settings/email_setting.html:47 +#: settings/templates/settings/ldap_setting.html:48 +#: settings/templates/settings/security_setting.html:55 +#: settings/templates/settings/terminal_setting.html:55 +#: terminal/templates/terminal/base_storage_create_update.html:13 #: terminal/templates/terminal/command_list.html:47 -#: terminal/templates/terminal/session_list.html:52 -#: terminal/templates/terminal/terminal_update.html:46 +#: terminal/templates/terminal/session_list.html:50 +#: terminal/templates/terminal/terminal_update.html:44 #: users/templates/users/_user.html:52 -#: users/templates/users/forgot_password.html:42 +#: users/templates/users/forgot_password.html:29 #: users/templates/users/user_bulk_update.html:24 -#: users/templates/users/user_list.html:57 +#: users/templates/users/user_list.html:40 #: users/templates/users/user_password_update.html:76 #: users/templates/users/user_profile_update.html:68 #: users/templates/users/user_pubkey_update.html:81 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:72 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:66 #: xpack/plugins/interface/templates/interface/interface.html:74 -#: xpack/plugins/vault/templates/vault/vault_create.html:46 +#: xpack/plugins/vault/templates/vault/vault_create.html:42 msgid "Submit" msgstr "提交" -#: applications/templates/applications/remote_app_detail.html:18 -#: assets/templates/assets/admin_user_assets.html:18 -#: assets/templates/assets/admin_user_detail.html:18 -#: assets/templates/assets/cmd_filter_detail.html:19 -#: assets/templates/assets/cmd_filter_rule_list.html:19 -#: assets/templates/assets/domain_detail.html:18 -#: assets/templates/assets/domain_gateway_list.html:20 -#: assets/templates/assets/system_user_assets.html:18 -#: assets/templates/assets/system_user_detail.html:18 -#: ops/templates/ops/adhoc_history.html:130 -#: ops/templates/ops/task_adhoc.html:116 -#: ops/templates/ops/task_history.html:137 -#: perms/templates/perms/asset_permission_asset.html:18 -#: perms/templates/perms/asset_permission_detail.html:18 -#: perms/templates/perms/asset_permission_user.html:18 -#: perms/templates/perms/remote_app_permission_detail.html:18 -#: perms/templates/perms/remote_app_permission_remote_app.html:17 -#: perms/templates/perms/remote_app_permission_user.html:17 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:17 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:20 +#: applications/templates/applications/database_app_detail.html:13 +#: applications/templates/applications/remote_app_detail.html:13 +#: assets/templates/assets/admin_user_assets.html:13 +#: assets/templates/assets/admin_user_detail.html:13 +#: assets/templates/assets/cmd_filter_detail.html:14 +#: assets/templates/assets/cmd_filter_rule_list.html:14 +#: assets/templates/assets/domain_detail.html:13 +#: assets/templates/assets/domain_gateway_list.html:15 +#: assets/templates/assets/platform_detail.html:13 +#: assets/templates/assets/system_user_assets.html:22 +#: assets/templates/assets/system_user_detail.html:13 +#: ops/templates/ops/adhoc_history.html:128 +#: ops/templates/ops/task_adhoc.html:114 +#: ops/templates/ops/task_history.html:135 +#: perms/templates/perms/asset_permission_asset.html:14 +#: perms/templates/perms/asset_permission_detail.html:13 +#: perms/templates/perms/asset_permission_user.html:14 +#: perms/templates/perms/database_app_permission_database_app.html:14 +#: perms/templates/perms/database_app_permission_detail.html:13 +#: perms/templates/perms/database_app_permission_user.html:14 +#: perms/templates/perms/remote_app_permission_detail.html:13 +#: perms/templates/perms/remote_app_permission_remote_app.html:13 +#: perms/templates/perms/remote_app_permission_user.html:13 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:13 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:18 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:17 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:106 #: xpack/plugins/change_auth_plan/views.py:91 msgid "Detail" msgstr "详情" -#: applications/templates/applications/remote_app_detail.html:21 -#: applications/templates/applications/remote_app_list.html:54 +#: applications/templates/applications/database_app_detail.html:16 +#: applications/templates/applications/database_app_list.html:53 +#: applications/templates/applications/remote_app_detail.html:16 +#: applications/templates/applications/remote_app_list.html:59 #: assets/templates/assets/_asset_user_list.html:75 -#: assets/templates/assets/admin_user_detail.html:24 -#: assets/templates/assets/admin_user_list.html:26 -#: assets/templates/assets/admin_user_list.html:74 -#: assets/templates/assets/asset_detail.html:26 -#: assets/templates/assets/asset_list.html:78 -#: assets/templates/assets/asset_list.html:167 -#: assets/templates/assets/cmd_filter_detail.html:29 -#: assets/templates/assets/cmd_filter_list.html:58 -#: assets/templates/assets/cmd_filter_rule_list.html:86 -#: assets/templates/assets/domain_detail.html:24 -#: assets/templates/assets/domain_detail.html:103 -#: assets/templates/assets/domain_gateway_list.html:97 -#: assets/templates/assets/domain_list.html:54 +#: assets/templates/assets/admin_user_detail.html:19 +#: assets/templates/assets/admin_user_list.html:46 +#: assets/templates/assets/asset_detail.html:24 +#: assets/templates/assets/asset_list.html:89 +#: assets/templates/assets/cmd_filter_detail.html:24 +#: assets/templates/assets/cmd_filter_list.html:56 +#: assets/templates/assets/cmd_filter_rule_list.html:81 +#: assets/templates/assets/domain_detail.html:19 +#: assets/templates/assets/domain_detail.html:98 +#: assets/templates/assets/domain_gateway_list.html:92 +#: assets/templates/assets/domain_list.html:50 #: assets/templates/assets/label_list.html:39 -#: assets/templates/assets/system_user_detail.html:26 -#: assets/templates/assets/system_user_list.html:29 -#: assets/templates/assets/system_user_list.html:81 audits/models.py:34 -#: perms/templates/perms/asset_permission_detail.html:30 -#: perms/templates/perms/asset_permission_list.html:178 -#: perms/templates/perms/remote_app_permission_detail.html:30 +#: assets/templates/assets/platform_detail.html:16 +#: assets/templates/assets/platform_list.html:40 +#: assets/templates/assets/system_user_detail.html:23 +#: assets/templates/assets/system_user_list.html:56 audits/models.py:34 +#: perms/templates/perms/asset_permission_detail.html:25 +#: perms/templates/perms/asset_permission_list.html:144 +#: perms/templates/perms/database_app_permission_detail.html:25 +#: perms/templates/perms/database_app_permission_list.html:64 +#: perms/templates/perms/remote_app_permission_detail.html:25 #: perms/templates/perms/remote_app_permission_list.html:64 +#: templates/_csv_import_export.html:18 templates/_csv_update_modal.html:6 +#: terminal/templates/terminal/base_storage_list.html:63 +#: terminal/templates/terminal/base_storage_list.html:70 #: terminal/templates/terminal/terminal_detail.html:16 -#: terminal/templates/terminal/terminal_list.html:73 -#: users/templates/users/user_detail.html:25 -#: users/templates/users/user_group_detail.html:28 -#: users/templates/users/user_group_list.html:20 -#: users/templates/users/user_group_list.html:71 -#: users/templates/users/user_list.html:20 -#: users/templates/users/user_list.html:103 -#: users/templates/users/user_list.html:106 +#: terminal/templates/terminal/terminal_list.html:74 +#: users/templates/users/user_asset_permission.html:127 +#: users/templates/users/user_database_app_permission.html:110 +#: users/templates/users/user_detail.html:12 +#: users/templates/users/user_group_detail.html:23 +#: users/templates/users/user_group_list.html:51 +#: users/templates/users/user_list.html:84 +#: users/templates/users/user_list.html:87 #: users/templates/users/user_profile.html:181 #: users/templates/users/user_profile.html:191 #: users/templates/users/user_profile.html:201 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:29 +#: users/templates/users/user_remote_app_permission.html:110 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:27 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:56 -#: xpack/plugins/cloud/templates/cloud/account_detail.html:23 +#: xpack/plugins/cloud/templates/cloud/account_detail.html:20 #: xpack/plugins/cloud/templates/cloud/account_list.html:40 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:29 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:26 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:57 #: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:46 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:25 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:20 #: xpack/plugins/orgs/templates/orgs/org_list.html:93 msgid "Update" msgstr "更新" -#: applications/templates/applications/remote_app_detail.html:25 -#: applications/templates/applications/remote_app_list.html:55 -#: assets/templates/assets/admin_user_detail.html:28 -#: assets/templates/assets/admin_user_list.html:75 -#: assets/templates/assets/asset_detail.html:30 -#: assets/templates/assets/asset_list.html:168 -#: assets/templates/assets/cmd_filter_detail.html:33 -#: assets/templates/assets/cmd_filter_list.html:59 -#: assets/templates/assets/cmd_filter_rule_list.html:87 -#: assets/templates/assets/domain_detail.html:28 -#: assets/templates/assets/domain_detail.html:104 -#: assets/templates/assets/domain_gateway_list.html:98 -#: assets/templates/assets/domain_list.html:55 +#: applications/templates/applications/database_app_detail.html:20 +#: applications/templates/applications/database_app_list.html:54 +#: applications/templates/applications/remote_app_detail.html:20 +#: applications/templates/applications/remote_app_list.html:60 +#: assets/templates/assets/admin_user_detail.html:23 +#: assets/templates/assets/admin_user_list.html:47 +#: assets/templates/assets/asset_detail.html:28 +#: assets/templates/assets/asset_list.html:90 +#: assets/templates/assets/cmd_filter_detail.html:28 +#: assets/templates/assets/cmd_filter_list.html:57 +#: assets/templates/assets/cmd_filter_rule_list.html:82 +#: assets/templates/assets/domain_detail.html:23 +#: assets/templates/assets/domain_detail.html:99 +#: assets/templates/assets/domain_gateway_list.html:93 +#: assets/templates/assets/domain_list.html:51 #: assets/templates/assets/label_list.html:40 -#: assets/templates/assets/system_user_detail.html:30 -#: assets/templates/assets/system_user_list.html:82 audits/models.py:35 +#: assets/templates/assets/platform_list.html:41 +#: assets/templates/assets/system_user_detail.html:27 +#: assets/templates/assets/system_user_list.html:57 audits/models.py:35 #: authentication/templates/authentication/_access_key_modal.html:65 -#: ops/templates/ops/task_list.html:69 -#: perms/templates/perms/asset_permission_detail.html:34 -#: perms/templates/perms/asset_permission_list.html:179 -#: perms/templates/perms/remote_app_permission_detail.html:34 +#: ops/templates/ops/task_list.html:74 +#: perms/templates/perms/asset_permission_detail.html:29 +#: perms/templates/perms/asset_permission_list.html:145 +#: perms/templates/perms/database_app_permission_detail.html:29 +#: perms/templates/perms/database_app_permission_list.html:65 +#: perms/templates/perms/remote_app_permission_detail.html:29 #: perms/templates/perms/remote_app_permission_list.html:65 -#: settings/templates/settings/terminal_setting.html:93 -#: settings/templates/settings/terminal_setting.html:115 -#: terminal/templates/terminal/terminal_list.html:75 -#: users/templates/users/user_detail.html:30 -#: users/templates/users/user_group_detail.html:32 -#: users/templates/users/user_group_list.html:73 -#: users/templates/users/user_list.html:111 -#: users/templates/users/user_list.html:115 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:33 +#: terminal/templates/terminal/base_storage_list.html:60 +#: terminal/templates/terminal/base_storage_list.html:67 +#: terminal/templates/terminal/terminal_list.html:76 +#: users/templates/users/user_asset_permission.html:128 +#: users/templates/users/user_database_app_permission.html:111 +#: users/templates/users/user_detail.html:16 +#: users/templates/users/user_group_detail.html:27 +#: users/templates/users/user_group_list.html:53 +#: users/templates/users/user_list.html:94 +#: users/templates/users/user_list.html:98 +#: users/templates/users/user_remote_app_permission.html:111 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:31 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:58 -#: xpack/plugins/cloud/templates/cloud/account_detail.html:27 +#: xpack/plugins/cloud/templates/cloud/account_detail.html:24 #: xpack/plugins/cloud/templates/cloud/account_list.html:42 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:33 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:30 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:58 #: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:47 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:29 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:24 #: xpack/plugins/orgs/templates/orgs/org_list.html:95 msgid "Delete" msgstr "删除" -#: applications/templates/applications/remote_app_list.html:5 -msgid "" -"Before using this feature, make sure that the application loader has been " -"uploaded to the application server and successfully published as a RemoteApp " -"application" -msgstr "" -"使用此功能前,请确保已将应用加载器上传到应用服务器并成功发布为一个 RemoteApp " -"应用" - -#: applications/templates/applications/remote_app_list.html:6 -msgid "Download application loader" -msgstr "下载应用加载器" - -#: applications/templates/applications/remote_app_list.html:12 -#: applications/views/remote_app.py:48 -msgid "Create RemoteApp" -msgstr "创建远程应用" +#: applications/templates/applications/database_app_list.html:9 +#: applications/views/database_app.py:69 applications/views/database_app.py:84 +msgid "Create DatabaseApp" +msgstr "创建数据库应用" -#: applications/templates/applications/remote_app_list.html:24 +#: applications/templates/applications/database_app_list.html:29 +#: applications/templates/applications/remote_app_list.html:29 +#: applications/templates/applications/user_database_app_list.html:21 #: applications/templates/applications/user_remote_app_list.html:20 #: assets/models/cmd_filter.py:55 #: assets/templates/assets/_asset_user_list.html:25 -#: assets/templates/assets/admin_user_list.html:51 -#: assets/templates/assets/asset_list.html:100 -#: assets/templates/assets/cmd_filter_list.html:28 -#: assets/templates/assets/cmd_filter_rule_list.html:63 -#: assets/templates/assets/domain_gateway_list.html:73 -#: assets/templates/assets/domain_list.html:29 +#: assets/templates/assets/admin_user_list.html:25 +#: assets/templates/assets/asset_list.html:28 +#: assets/templates/assets/cmd_filter_list.html:26 +#: assets/templates/assets/cmd_filter_rule_list.html:58 +#: assets/templates/assets/domain_gateway_list.html:68 +#: assets/templates/assets/domain_list.html:25 #: assets/templates/assets/label_list.html:17 -#: assets/templates/assets/system_user_list.html:56 audits/models.py:39 -#: audits/templates/audits/operate_log_list.html:47 -#: audits/templates/audits/operate_log_list.html:73 +#: assets/templates/assets/platform_list.html:19 +#: assets/templates/assets/system_user_list.html:33 audits/models.py:39 +#: audits/templates/audits/operate_log_list.html:45 +#: audits/templates/audits/operate_log_list.html:71 #: authentication/templates/authentication/_access_key_modal.html:34 -#: ops/templates/ops/adhoc_history.html:59 ops/templates/ops/task_adhoc.html:64 -#: ops/templates/ops/task_history.html:65 ops/templates/ops/task_list.html:18 -#: perms/forms/asset_permission.py:21 -#: perms/templates/perms/asset_permission_create_update.html:50 -#: perms/templates/perms/asset_permission_list.html:56 -#: perms/templates/perms/asset_permission_list.html:130 +#: ops/templates/ops/adhoc_history.html:57 ops/templates/ops/task_adhoc.html:62 +#: ops/templates/ops/task_history.html:63 ops/templates/ops/task_list.html:17 +#: perms/forms/asset_permission.py:20 +#: perms/templates/perms/asset_permission_asset.html:54 +#: perms/templates/perms/asset_permission_create_update.html:63 +#: perms/templates/perms/asset_permission_list.html:39 +#: perms/templates/perms/asset_permission_list.html:96 +#: perms/templates/perms/asset_permission_user.html:54 +#: perms/templates/perms/database_app_permission_database_app.html:54 +#: perms/templates/perms/database_app_permission_list.html:20 +#: perms/templates/perms/database_app_permission_user.html:54 #: perms/templates/perms/remote_app_permission_list.html:20 -#: settings/templates/settings/terminal_setting.html:85 -#: settings/templates/settings/terminal_setting.html:107 -#: terminal/templates/terminal/session_list.html:36 -#: terminal/templates/terminal/terminal_list.html:36 +#: terminal/templates/terminal/base_storage_list.html:34 +#: terminal/templates/terminal/session_list.html:34 +#: terminal/templates/terminal/terminal_list.html:37 +#: tickets/templates/tickets/ticket_list.html:108 #: users/templates/users/_granted_assets.html:34 -#: users/templates/users/user_group_list.html:38 -#: users/templates/users/user_list.html:41 +#: users/templates/users/user_asset_permission.html:44 +#: users/templates/users/user_asset_permission.html:79 +#: users/templates/users/user_database_app_permission.html:42 +#: users/templates/users/user_group_list.html:17 +#: users/templates/users/user_list.html:20 +#: users/templates/users/user_remote_app_permission.html:42 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:60 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:18 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:20 #: xpack/plugins/cloud/templates/cloud/account_list.html:16 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:72 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:67 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:19 #: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:20 #: xpack/plugins/orgs/templates/orgs/org_list.html:24 msgid "Action" msgstr "动作" +#: applications/templates/applications/remote_app_list.html:4 +msgid "" +"Before using this feature, make sure that the application loader has been " +"uploaded to the application server and successfully published as a RemoteApp " +"application" +msgstr "" +"使用此功能前,请确保已将应用加载器上传到应用服务器并成功发布为一个 RemoteApp " +"应用" + +#: applications/templates/applications/remote_app_list.html:5 +msgid "Download application loader" +msgstr "下载应用加载器" + +#: applications/templates/applications/remote_app_list.html:11 +#: applications/views/remote_app.py:69 +msgid "Create RemoteApp" +msgstr "创建远程应用" + +#: applications/templates/applications/user_database_app_list.html:61 #: applications/templates/applications/user_remote_app_list.html:52 #: perms/models/asset_permission.py:32 msgid "Connect" msgstr "连接" -#: applications/views/remote_app.py:31 applications/views/remote_app.py:47 -#: applications/views/remote_app.py:70 applications/views/remote_app.py:89 -#: templates/_nav.html:57 +#: applications/views/database_app.py:26 users/models/user.py:144 +msgid "Application" +msgstr "应用程序" + +#: applications/views/database_app.py:27 +msgid "DatabaseApp list" +msgstr "数据库应用列表" + +#: applications/views/database_app.py:68 applications/views/database_app.py:83 +#: applications/views/database_app.py:99 applications/views/remote_app.py:28 +#: applications/views/remote_app.py:68 applications/views/remote_app.py:96 +#: applications/views/remote_app.py:112 templates/_nav.html:60 msgid "Applications" msgstr "应用管理" -#: applications/views/remote_app.py:32 +#: applications/views/database_app.py:100 +msgid "DatabaseApp detail" +msgstr "数据库应用详情" + +#: applications/views/database_app.py:112 +msgid "My DatabaseApp" +msgstr "我的数据库应用" + +#: applications/views/remote_app.py:29 msgid "RemoteApp list" msgstr "远程应用列表" -#: applications/views/remote_app.py:71 +#: applications/views/remote_app.py:97 msgid "Update RemoteApp" msgstr "更新远程应用" -#: applications/views/remote_app.py:90 +#: applications/views/remote_app.py:113 msgid "RemoteApp detail" msgstr "远程应用详情" -#: applications/views/remote_app.py:102 +#: applications/views/remote_app.py:125 msgid "My RemoteApp" msgstr "我的远程应用" +#: assets/api/admin_user.py:59 +msgid "Deleted failed, There are related assets" +msgstr "删除失败,存在关联资产" + #: assets/api/node.py:61 msgid "You can't update the root node name" msgstr "不能修改根节点名称" @@ -582,25 +728,10 @@ msgstr "更新节点资产硬件信息: {}" msgid "Test if the assets under the node are connectable: {}" msgstr "测试节点下资产是否可连接: {}" -#: assets/const.py:8 -msgid "Cannot contain special characters: [ {} ]" -msgstr "不能包含特殊字符:[ {} ]" - -#: assets/const.py:14 -msgid "* The contains characters that are not allowed" -msgstr "* 包含不被允许的字符" - -#: assets/forms/asset.py:25 assets/models/asset.py:140 -#: assets/models/domain.py:50 -#: assets/templates/assets/domain_gateway_list.html:69 -#: settings/templates/settings/replay_storage_create.html:59 -msgid "Port" -msgstr "端口" - -#: assets/forms/asset.py:56 assets/models/asset.py:145 -#: assets/models/user.py:110 assets/templates/assets/asset_detail.html:188 -#: assets/templates/assets/asset_detail.html:196 -#: assets/templates/assets/system_user_assets.html:83 +#: assets/forms/asset.py:65 assets/models/asset.py:194 +#: assets/models/user.py:111 assets/templates/assets/asset_detail.html:186 +#: assets/templates/assets/asset_detail.html:194 +#: assets/templates/assets/system_user_assets.html:87 #: perms/models/asset_permission.py:81 #: xpack/plugins/change_auth_plan/models.py:74 #: xpack/plugins/gathered_user/models.py:31 @@ -608,49 +739,59 @@ msgstr "端口" msgid "Nodes" msgstr "节点" -#: assets/forms/asset.py:59 assets/forms/asset.py:106 -#: assets/models/asset.py:149 assets/models/cluster.py:19 -#: assets/models/user.py:68 assets/templates/assets/asset_detail.html:74 -#: templates/_nav.html:44 xpack/plugins/cloud/models.py:161 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:68 +#: assets/forms/asset.py:68 assets/models/asset.py:198 +#: assets/models/cluster.py:19 assets/models/user.py:67 +#: assets/templates/assets/admin_user_list.html:62 +#: assets/templates/assets/asset_detail.html:72 templates/_nav.html:44 +#: xpack/plugins/cloud/models.py:160 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:65 #: xpack/plugins/orgs/templates/orgs/org_list.html:19 msgid "Admin user" msgstr "管理用户" -#: assets/forms/asset.py:62 assets/forms/asset.py:109 assets/forms/asset.py:149 +#: assets/forms/asset.py:71 assets/forms/asset.py:113 #: assets/templates/assets/asset_create.html:48 #: assets/templates/assets/asset_create.html:50 -#: assets/templates/assets/asset_list.html:85 +#: assets/templates/assets/asset_list.html:13 #: xpack/plugins/orgs/templates/orgs/org_list.html:21 msgid "Label" msgstr "标签" -#: assets/forms/asset.py:65 assets/forms/asset.py:112 -#: assets/models/asset.py:144 assets/models/domain.py:26 -#: assets/models/domain.py:52 assets/templates/assets/asset_detail.html:78 +#: assets/forms/asset.py:74 assets/models/asset.py:193 +#: assets/models/domain.py:26 assets/models/domain.py:52 +#: assets/templates/assets/asset_detail.html:76 #: assets/templates/assets/user_asset_list.html:80 #: xpack/plugins/orgs/templates/orgs/org_list.html:18 msgid "Domain" msgstr "网域" -#: assets/forms/asset.py:69 assets/forms/asset.py:103 assets/forms/asset.py:116 -#: assets/forms/asset.py:152 assets/models/node.py:462 -#: assets/serializers/system_user.py:36 +#: assets/forms/asset.py:77 assets/models/asset.py:168 +#: assets/models/asset.py:192 assets/serializers/asset.py:66 +#: assets/templates/assets/asset_detail.html:100 +#: assets/templates/assets/user_asset_list.html:78 +msgid "Platform" +msgstr "系统平台" + +#: assets/forms/asset.py:81 assets/forms/asset.py:116 assets/models/node.py:462 +#: assets/serializers/system_user.py:40 #: assets/templates/assets/asset_create.html:42 -#: perms/forms/asset_permission.py:87 perms/forms/asset_permission.py:94 -#: perms/templates/perms/asset_permission_list.html:53 -#: perms/templates/perms/asset_permission_list.html:74 -#: perms/templates/perms/asset_permission_list.html:124 +#: perms/forms/asset_permission.py:92 perms/forms/asset_permission.py:99 +#: perms/templates/perms/asset_permission_list.html:36 +#: perms/templates/perms/asset_permission_list.html:90 +#: perms/templates/perms/asset_permission_list.html:189 +#: users/templates/users/user_asset_permission.html:41 +#: users/templates/users/user_asset_permission.html:73 +#: users/templates/users/user_asset_permission.html:158 #: xpack/plugins/change_auth_plan/forms.py:74 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:55 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:15 -#: xpack/plugins/cloud/models.py:157 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:64 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:64 +#: xpack/plugins/cloud/models.py:156 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:61 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:61 msgid "Node" msgstr "节点" -#: assets/forms/asset.py:74 assets/forms/asset.py:121 +#: assets/forms/asset.py:85 msgid "" "root or other NOPASSWD sudo privilege user existed in asset,If asset is " "windows or other set any one, more see admin user left menu" @@ -658,20 +799,20 @@ msgstr "" "root或其他拥有NOPASSWD: ALL权限的用户, 如果是windows或其它硬件可以随意设置一" "个, 更多信息查看左侧 `管理用户` 菜单" -#: assets/forms/asset.py:77 assets/forms/asset.py:124 +#: assets/forms/asset.py:88 msgid "Windows 2016 RDP protocol is different, If is window 2016, set it" msgstr "Windows 2016的RDP协议与之前不同,如果是请设置" -#: assets/forms/asset.py:78 assets/forms/asset.py:125 +#: assets/forms/asset.py:89 msgid "" "If your have some network not connect with each other, you can set domain" msgstr "如果有多个的互相隔离的网络,设置资产属于的网域,使用网域网关跳转登录" -#: assets/forms/asset.py:132 assets/forms/asset.py:136 -#: assets/forms/domain.py:17 assets/forms/label.py:15 -#: perms/templates/perms/asset_permission_asset.html:78 +#: assets/forms/asset.py:96 assets/forms/asset.py:100 assets/forms/domain.py:17 +#: assets/forms/label.py:15 +#: perms/templates/perms/asset_permission_asset.html:74 #: xpack/plugins/change_auth_plan/forms.py:64 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:74 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:70 msgid "Select assets" msgstr "选择资产" @@ -681,58 +822,84 @@ msgstr "内容不能包含: {}" #: assets/forms/domain.py:55 assets/models/domain.py:67 msgid "Password should not contain special characters" -msgstr "密码不能包含特殊字符" +msgstr "不能包含特殊字符" #: assets/forms/domain.py:74 msgid "SSH gateway support proxy SSH,RDP,VNC" msgstr "SSH网关,支持代理SSH,RDP和VNC" -#: assets/forms/domain.py:78 assets/forms/user.py:76 assets/forms/user.py:96 -#: assets/models/base.py:29 assets/models/gathered_user.py:16 +#: assets/forms/domain.py:78 assets/forms/user.py:75 assets/forms/user.py:95 +#: assets/models/base.py:29 assets/models/gathered_user.py:15 #: assets/templates/assets/_asset_user_auth_update_modal.html:15 #: assets/templates/assets/_asset_user_auth_view_modal.html:21 #: assets/templates/assets/_asset_user_list.html:21 -#: assets/templates/assets/admin_user_detail.html:60 -#: assets/templates/assets/admin_user_list.html:45 -#: assets/templates/assets/domain_gateway_list.html:71 -#: assets/templates/assets/system_user_detail.html:62 -#: assets/templates/assets/system_user_list.html:48 audits/models.py:81 -#: audits/templates/audits/login_log_list.html:57 authentication/forms.py:13 -#: authentication/templates/authentication/login.html:65 -#: authentication/templates/authentication/new_login.html:92 -#: ops/models/adhoc.py:189 perms/templates/perms/asset_permission_list.html:70 -#: perms/templates/perms/asset_permission_user.html:55 -#: perms/templates/perms/remote_app_permission_user.html:54 -#: settings/templates/settings/_ldap_list_users_modal.html:31 users/forms.py:14 -#: users/models/user.py:371 users/templates/users/_select_user_modal.html:14 -#: users/templates/users/user_detail.html:67 -#: users/templates/users/user_list.html:36 +#: assets/templates/assets/admin_user_detail.html:55 +#: assets/templates/assets/admin_user_list.html:22 +#: assets/templates/assets/domain_gateway_list.html:66 +#: assets/templates/assets/system_user_detail.html:59 +#: assets/templates/assets/system_user_list.html:25 audits/models.py:81 +#: audits/templates/audits/login_log_list.html:57 authentication/forms.py:10 +#: authentication/templates/authentication/login.html:58 +#: authentication/templates/authentication/xpack_login.html:93 +#: ops/models/adhoc.py:187 perms/templates/perms/asset_permission_list.html:185 +#: perms/templates/perms/remote_app_permission_user.html:50 +#: settings/templates/settings/_ldap_list_users_modal.html:31 +#: users/forms/profile.py:19 users/models/user.py:436 +#: users/templates/users/_select_user_modal.html:14 +#: users/templates/users/user_detail.html:53 +#: users/templates/users/user_list.html:15 #: users/templates/users/user_profile.html:47 #: xpack/plugins/change_auth_plan/forms.py:58 #: xpack/plugins/change_auth_plan/models.py:65 -#: xpack/plugins/change_auth_plan/models.py:408 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:65 +#: xpack/plugins/change_auth_plan/models.py:414 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:63 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:53 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:12 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:13 -#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:74 +#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:25 msgid "Username" msgstr "用户名" -#: assets/forms/user.py:26 +#: assets/forms/platform.py:20 ops/templates/ops/task_detail.html:85 +#: ops/templates/ops/task_detail.html:95 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:82 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:72 +msgid "Yes" +msgstr "是" + +#: assets/forms/platform.py:21 ops/templates/ops/task_detail.html:87 +#: ops/templates/ops/task_detail.html:97 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:84 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:74 +msgid "No" +msgstr "否" + +#: assets/forms/platform.py:24 +msgid "RDP security" +msgstr "" + +#: assets/forms/platform.py:28 +msgid "RDP console" +msgstr "" + +#: assets/forms/platform.py:40 assets/templates/assets/platform_detail.html:47 +#: assets/templates/assets/platform_list.html:17 +msgid "Base platform" +msgstr "基础平台" + +#: assets/forms/user.py:25 msgid "Password or private key passphrase" msgstr "密码或密钥密码" -#: assets/forms/user.py:27 assets/models/base.py:30 +#: assets/forms/user.py:26 assets/models/base.py:30 #: assets/serializers/asset_user.py:63 #: assets/templates/assets/_asset_user_auth_update_modal.html:21 #: assets/templates/assets/_asset_user_auth_view_modal.html:27 -#: authentication/forms.py:15 -#: authentication/templates/authentication/login.html:68 -#: authentication/templates/authentication/new_login.html:95 -#: settings/forms.py:114 users/forms.py:16 users/forms.py:42 -#: users/templates/users/reset_password.html:53 -#: users/templates/users/user_password_authentication.html:18 +#: authentication/forms.py:12 +#: authentication/templates/authentication/login.html:66 +#: authentication/templates/authentication/xpack_login.html:101 +#: settings/forms/ldap.py:22 users/forms/user.py:22 users/forms/user.py:193 +#: users/templates/users/user_password_check.html:13 #: users/templates/users/user_password_update.html:44 #: users/templates/users/user_profile_update.html:41 #: users/templates/users/user_pubkey_update.html:41 @@ -742,31 +909,31 @@ msgstr "密码或密钥密码" msgid "Password" msgstr "密码" -#: assets/forms/user.py:30 assets/serializers/asset_user.py:71 +#: assets/forms/user.py:29 assets/serializers/asset_user.py:71 #: assets/templates/assets/_asset_user_auth_update_modal.html:27 -#: users/models/user.py:400 +#: users/models/user.py:465 msgid "Private key" msgstr "ssh私钥" -#: assets/forms/user.py:42 +#: assets/forms/user.py:41 msgid "Invalid private key, Only support RSA/DSA format key" msgstr "不合法的密钥,仅支持RSA/DSA格式的密钥" -#: assets/forms/user.py:53 +#: assets/forms/user.py:52 msgid "Password and private key file must be input one" msgstr "密码和私钥, 必须输入一个" -#: assets/forms/user.py:98 assets/models/cmd_filter.py:32 -#: assets/models/user.py:118 assets/templates/assets/_system_user.html:66 -#: assets/templates/assets/system_user_detail.html:165 +#: assets/forms/user.py:97 assets/models/cmd_filter.py:32 +#: assets/models/user.py:119 assets/templates/assets/_system_user.html:62 +#: assets/templates/assets/system_user_detail.html:164 msgid "Command filter" msgstr "命令过滤器" -#: assets/forms/user.py:103 +#: assets/forms/user.py:101 msgid "Auto push system user to asset" msgstr "自动推送系统用户到资产" -#: assets/forms/user.py:104 +#: assets/forms/user.py:102 msgid "" "1-100, High level will be using login asset as default, if user was granted " "more than 2 system user" @@ -774,156 +941,170 @@ msgstr "" "1-100, 1最低优先级,100最高优先级。授权多个用户时,高优先级的系统用户将会作为" "默认登录用户" -#: assets/forms/user.py:106 +#: assets/forms/user.py:104 msgid "" "If you choose manual login mode, you do not need to fill in the username and " "password." msgstr "如果选择手动登录模式,用户名和密码可以不填写" -#: assets/forms/user.py:108 +#: assets/forms/user.py:106 msgid "Use comma split multi command, ex: /bin/whoami,/bin/ifconfig" msgstr "使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig" -#: assets/models/asset.py:135 assets/models/domain.py:49 +#: assets/models/asset.py:145 +msgid "Base" +msgstr "基础" + +#: assets/models/asset.py:146 assets/templates/assets/platform_detail.html:51 +msgid "Charset" +msgstr "编码" + +#: assets/models/asset.py:147 assets/templates/assets/platform_detail.html:55 +#: tickets/models/ticket.py:38 +msgid "Meta" +msgstr "元数据" + +#: assets/models/asset.py:148 +msgid "Internal" +msgstr "内部的" + +#: assets/models/asset.py:185 assets/models/domain.py:49 #: assets/serializers/asset_user.py:28 #: assets/templates/assets/_asset_list_modal.html:47 #: assets/templates/assets/_asset_user_list.html:20 -#: assets/templates/assets/asset_detail.html:62 -#: assets/templates/assets/asset_list.html:97 -#: assets/templates/assets/domain_gateway_list.html:68 +#: assets/templates/assets/asset_detail.html:60 +#: assets/templates/assets/asset_list.html:25 +#: assets/templates/assets/domain_gateway_list.html:63 #: assets/templates/assets/user_asset_list.html:76 #: audits/templates/audits/login_log_list.html:60 -#: perms/templates/perms/asset_permission_asset.html:58 settings/forms.py:144 -#: users/templates/users/_granted_assets.html:31 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:54 -#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:73 +#: perms/templates/perms/asset_permission_list.html:187 +#: settings/forms/terminal.py:16 users/templates/users/_granted_assets.html:31 +#: users/templates/users/user_asset_permission.html:156 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:50 +#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:24 msgid "IP" msgstr "IP" -#: assets/models/asset.py:136 assets/serializers/asset_user.py:27 -#: assets/serializers/gathered_user.py:19 +#: assets/models/asset.py:186 assets/serializers/asset_user.py:27 +#: assets/serializers/gathered_user.py:20 #: assets/templates/assets/_asset_list_modal.html:46 #: assets/templates/assets/_asset_user_auth_update_modal.html:9 #: assets/templates/assets/_asset_user_auth_view_modal.html:15 #: assets/templates/assets/_asset_user_list.html:19 -#: assets/templates/assets/asset_detail.html:58 -#: assets/templates/assets/asset_list.html:96 +#: assets/templates/assets/asset_detail.html:56 +#: assets/templates/assets/asset_list.html:24 #: assets/templates/assets/user_asset_list.html:75 -#: perms/templates/perms/asset_permission_asset.html:57 -#: perms/templates/perms/asset_permission_list.html:73 settings/forms.py:143 -#: users/templates/users/_granted_assets.html:30 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:53 -#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:72 +#: perms/templates/perms/asset_permission_list.html:188 +#: settings/forms/terminal.py:15 users/templates/users/_granted_assets.html:30 +#: users/templates/users/user_asset_permission.html:157 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:49 +#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:23 msgid "Hostname" msgstr "主机名" -#: assets/models/asset.py:139 assets/models/domain.py:51 -#: assets/models/user.py:113 assets/templates/assets/asset_detail.html:70 -#: assets/templates/assets/domain_gateway_list.html:70 -#: assets/templates/assets/system_user_detail.html:70 -#: assets/templates/assets/system_user_list.html:49 -#: terminal/templates/terminal/session_list.html:31 -#: terminal/templates/terminal/session_list.html:75 +#: assets/models/asset.py:189 assets/models/domain.py:51 +#: assets/models/user.py:114 assets/templates/assets/asset_detail.html:68 +#: assets/templates/assets/domain_gateway_list.html:65 +#: assets/templates/assets/system_user_detail.html:67 +#: assets/templates/assets/system_user_list.html:26 +#: terminal/forms/storage.py:152 +#: terminal/templates/terminal/session_list.html:29 +#: terminal/templates/terminal/session_list.html:73 msgid "Protocol" msgstr "协议" -#: assets/models/asset.py:142 assets/serializers/asset.py:68 +#: assets/models/asset.py:191 assets/serializers/asset.py:68 #: assets/templates/assets/asset_create.html:24 #: assets/templates/assets/user_asset_list.html:77 -#: perms/serializers/user_permission.py:48 +#: perms/serializers/user_permission.py:59 msgid "Protocols" msgstr "协议组" -#: assets/models/asset.py:143 assets/templates/assets/asset_detail.html:102 -#: assets/templates/assets/user_asset_list.html:78 -msgid "Platform" -msgstr "系统平台" - -#: assets/models/asset.py:146 assets/models/authbook.py:27 +#: assets/models/asset.py:195 assets/models/authbook.py:27 #: assets/models/cmd_filter.py:22 assets/models/domain.py:54 -#: assets/models/label.py:22 assets/templates/assets/asset_detail.html:110 +#: assets/models/label.py:22 assets/templates/assets/asset_detail.html:108 +#: authentication/models.py:45 msgid "Is active" msgstr "激活" -#: assets/models/asset.py:152 assets/templates/assets/asset_detail.html:66 +#: assets/models/asset.py:201 assets/templates/assets/asset_detail.html:64 msgid "Public IP" msgstr "公网IP" -#: assets/models/asset.py:153 assets/templates/assets/asset_detail.html:118 +#: assets/models/asset.py:202 assets/templates/assets/asset_detail.html:116 msgid "Asset number" msgstr "资产编号" -#: assets/models/asset.py:156 assets/templates/assets/asset_detail.html:82 +#: assets/models/asset.py:205 assets/templates/assets/asset_detail.html:80 msgid "Vendor" msgstr "制造商" -#: assets/models/asset.py:157 assets/templates/assets/asset_detail.html:86 +#: assets/models/asset.py:206 assets/templates/assets/asset_detail.html:84 msgid "Model" msgstr "型号" -#: assets/models/asset.py:158 assets/templates/assets/asset_detail.html:114 +#: assets/models/asset.py:207 assets/templates/assets/asset_detail.html:112 msgid "Serial number" msgstr "序列号" -#: assets/models/asset.py:160 +#: assets/models/asset.py:209 msgid "CPU model" msgstr "CPU型号" -#: assets/models/asset.py:161 -#: xpack/plugins/license/templates/license/license_detail.html:80 +#: assets/models/asset.py:210 msgid "CPU count" msgstr "CPU数量" -#: assets/models/asset.py:162 +#: assets/models/asset.py:211 msgid "CPU cores" msgstr "CPU核数" -#: assets/models/asset.py:163 +#: assets/models/asset.py:212 msgid "CPU vcpus" msgstr "CPU总数" -#: assets/models/asset.py:164 assets/templates/assets/asset_detail.html:94 +#: assets/models/asset.py:213 assets/templates/assets/asset_detail.html:92 msgid "Memory" msgstr "内存" -#: assets/models/asset.py:165 +#: assets/models/asset.py:214 msgid "Disk total" msgstr "硬盘大小" -#: assets/models/asset.py:166 +#: assets/models/asset.py:215 msgid "Disk info" msgstr "硬盘信息" -#: assets/models/asset.py:168 assets/templates/assets/asset_detail.html:106 +#: assets/models/asset.py:217 assets/templates/assets/asset_detail.html:104 msgid "OS" msgstr "操作系统" -#: assets/models/asset.py:169 +#: assets/models/asset.py:218 msgid "OS version" msgstr "系统版本" -#: assets/models/asset.py:170 +#: assets/models/asset.py:219 msgid "OS arch" msgstr "系统架构" -#: assets/models/asset.py:171 +#: assets/models/asset.py:220 msgid "Hostname raw" msgstr "主机名原始" -#: assets/models/asset.py:173 assets/templates/assets/asset_create.html:46 -#: assets/templates/assets/asset_detail.html:222 templates/_nav.html:46 +#: assets/models/asset.py:222 assets/templates/assets/asset_create.html:46 +#: assets/templates/assets/asset_detail.html:220 templates/_nav.html:46 msgid "Labels" msgstr "标签管理" -#: assets/models/authbook.py:25 ops/templates/ops/task_detail.html:72 +#: assets/models/authbook.py:25 ops/templates/ops/task_detail.html:70 msgid "Latest version" msgstr "最新版本" #: assets/models/authbook.py:26 #: assets/templates/assets/_asset_user_list.html:22 -#: ops/templates/ops/adhoc_history.html:58 -#: ops/templates/ops/adhoc_history_detail.html:57 -#: ops/templates/ops/task_adhoc.html:58 ops/templates/ops/task_history.html:64 +#: ops/templates/ops/adhoc_history.html:56 +#: ops/templates/ops/adhoc_history_detail.html:55 +#: ops/templates/ops/task_adhoc.html:56 ops/templates/ops/task_history.html:62 msgid "Version" msgstr "版本" @@ -941,11 +1122,11 @@ msgstr "ssh密钥" msgid "SSH public key" msgstr "ssh公钥" -#: assets/models/base.py:35 assets/models/gathered_user.py:21 -#: assets/templates/assets/cmd_filter_detail.html:73 common/mixins/models.py:52 -#: ops/models/adhoc.py:46 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:109 -#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:76 +#: assets/models/base.py:35 assets/models/gathered_user.py:20 +#: assets/templates/assets/cmd_filter_detail.html:68 common/mixins/models.py:52 +#: ops/models/adhoc.py:49 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:107 +#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:29 msgid "Date updated" msgstr "更新日期" @@ -957,8 +1138,8 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:392 -#: users/templates/users/user_detail.html:76 +#: assets/models/cluster.py:22 users/models/user.py:457 +#: users/templates/users/user_detail.html:62 msgid "Phone" msgstr "手机" @@ -983,7 +1164,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:512 +#: users/models/user.py:580 msgid "System" msgstr "系统" @@ -1011,12 +1192,12 @@ msgstr "BGP全网通" msgid "Regex" msgstr "正则表达式" -#: assets/models/cmd_filter.py:40 ops/models/command.py:21 -#: ops/templates/ops/command_execution_list.html:64 terminal/models.py:163 +#: assets/models/cmd_filter.py:40 ops/models/command.py:22 +#: ops/templates/ops/command_execution_list.html:67 terminal/models.py:186 #: terminal/templates/terminal/command_list.html:28 #: terminal/templates/terminal/command_list.html:68 #: terminal/templates/terminal/session_detail.html:48 -#: terminal/templates/terminal/session_list.html:33 +#: terminal/templates/terminal/session_list.html:31 msgid "Command" msgstr "命令" @@ -1032,19 +1213,8 @@ msgstr "允许" msgid "Filter" msgstr "过滤器" -#: assets/models/cmd_filter.py:51 -#: assets/templates/assets/cmd_filter_rule_list.html:58 -#: audits/templates/audits/login_log_list.html:58 -#: perms/templates/perms/remote_app_permission_remote_app.html:54 -#: settings/templates/settings/command_storage_create.html:31 -#: settings/templates/settings/replay_storage_create.html:31 -#: settings/templates/settings/terminal_setting.html:84 -#: settings/templates/settings/terminal_setting.html:106 -msgid "Type" -msgstr "类型" - -#: assets/models/cmd_filter.py:52 assets/models/user.py:112 -#: assets/templates/assets/cmd_filter_rule_list.html:60 +#: assets/models/cmd_filter.py:52 assets/models/user.py:113 +#: assets/templates/assets/cmd_filter_rule_list.html:55 msgid "Priority" msgstr "优先级" @@ -1053,7 +1223,7 @@ msgid "1-100, the higher will be match first" msgstr "优先级可选范围为1-100,1最低优先级,100最高优先级" #: assets/models/cmd_filter.py:54 -#: assets/templates/assets/cmd_filter_rule_list.html:59 +#: assets/templates/assets/cmd_filter_rule_list.html:54 #: xpack/plugins/license/models.py:29 msgid "Content" msgstr "内容" @@ -1066,19 +1236,29 @@ msgstr "每行一个命令" msgid "Command filter rule" msgstr "命令过滤规则" -#: assets/models/domain.py:61 assets/templates/assets/domain_detail.html:21 -#: assets/templates/assets/domain_detail.html:64 -#: assets/templates/assets/domain_gateway_list.html:26 -#: assets/templates/assets/domain_list.html:27 +#: assets/models/domain.py:61 assets/templates/assets/domain_detail.html:16 +#: assets/templates/assets/domain_detail.html:59 +#: assets/templates/assets/domain_gateway_list.html:21 +#: assets/templates/assets/domain_list.html:23 msgid "Gateway" msgstr "网关" -#: assets/models/gathered_user.py:17 -#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:75 +#: assets/models/gathered_user.py:16 +#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:28 msgid "Present" msgstr "存在" -#: assets/models/gathered_user.py:32 +#: assets/models/gathered_user.py:17 +#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:26 +msgid "Date last login" +msgstr "最后登录日期" + +#: assets/models/gathered_user.py:18 +#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:27 +msgid "IP last login" +msgstr "最后登录IP" + +#: assets/models/gathered_user.py:31 msgid "GatherUser" msgstr "收集用户" @@ -1091,38 +1271,48 @@ msgid "Default asset group" msgstr "默认资产组" #: assets/models/label.py:15 audits/models.py:18 audits/models.py:38 -#: audits/models.py:51 audits/templates/audits/ftp_log_list.html:36 -#: audits/templates/audits/ftp_log_list.html:73 -#: audits/templates/audits/operate_log_list.html:39 -#: audits/templates/audits/operate_log_list.html:72 -#: audits/templates/audits/password_change_log_list.html:39 -#: audits/templates/audits/password_change_log_list.html:56 -#: ops/templates/ops/command_execution_list.html:38 -#: ops/templates/ops/command_execution_list.html:63 -#: perms/forms/asset_permission.py:78 perms/forms/remote_app_permission.py:34 -#: perms/models/base.py:49 -#: perms/templates/perms/asset_permission_create_update.html:41 -#: perms/templates/perms/asset_permission_list.html:50 -#: perms/templates/perms/asset_permission_list.html:115 -#: perms/templates/perms/remote_app_permission_create_update.html:43 +#: audits/models.py:51 audits/templates/audits/ftp_log_list.html:37 +#: audits/templates/audits/ftp_log_list.html:74 +#: audits/templates/audits/operate_log_list.html:37 +#: audits/templates/audits/password_change_log_list.html:37 +#: audits/templates/audits/password_change_log_list.html:54 +#: authentication/models.py:43 ops/templates/ops/command_execution_list.html:41 +#: ops/templates/ops/command_execution_list.html:66 +#: perms/forms/asset_permission.py:83 perms/forms/database_app_permission.py:38 +#: perms/forms/remote_app_permission.py:40 perms/models/base.py:49 +#: perms/templates/perms/asset_permission_create_update.html:52 +#: perms/templates/perms/asset_permission_list.html:33 +#: perms/templates/perms/asset_permission_list.html:81 +#: perms/templates/perms/database_app_permission_create_update.html:41 +#: perms/templates/perms/database_app_permission_list.html:15 +#: perms/templates/perms/remote_app_permission_create_update.html:41 #: perms/templates/perms/remote_app_permission_list.html:15 #: templates/index.html:87 terminal/backends/command/models.py:12 -#: terminal/models.py:156 terminal/templates/terminal/command_list.html:29 +#: terminal/models.py:176 terminal/templates/terminal/command_list.html:29 #: terminal/templates/terminal/command_list.html:65 -#: terminal/templates/terminal/session_list.html:27 -#: terminal/templates/terminal/session_list.html:71 users/forms.py:339 -#: users/models/user.py:127 users/models/user.py:143 users/models/user.py:500 -#: users/serializers/group.py:21 -#: users/templates/users/user_group_detail.html:78 -#: users/templates/users/user_group_list.html:36 users/views/user.py:250 -#: xpack/plugins/orgs/forms.py:28 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:113 +#: terminal/templates/terminal/session_list.html:25 +#: terminal/templates/terminal/session_list.html:69 tickets/models/ticket.py:33 +#: tickets/models/ticket.py:128 tickets/templates/tickets/ticket_detail.html:32 +#: tickets/templates/tickets/ticket_list.html:34 +#: tickets/templates/tickets/ticket_list.html:103 users/forms/group.py:15 +#: users/models/user.py:143 users/models/user.py:159 users/models/user.py:568 +#: users/serializers/group.py:20 +#: users/templates/users/user_asset_permission.html:38 +#: users/templates/users/user_asset_permission.html:64 +#: users/templates/users/user_database_app_permission.html:37 +#: users/templates/users/user_database_app_permission.html:58 +#: users/templates/users/user_group_detail.html:73 +#: users/templates/users/user_group_list.html:15 +#: users/templates/users/user_remote_app_permission.html:37 +#: users/templates/users/user_remote_app_permission.html:58 +#: users/views/profile.py:70 xpack/plugins/orgs/forms.py:27 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:108 #: xpack/plugins/orgs/templates/orgs/org_list.html:15 msgid "User" msgstr "用户" #: assets/models/label.py:19 assets/models/node.py:453 -#: assets/templates/assets/label_list.html:15 settings/models.py:30 +#: assets/templates/assets/label_list.html:15 settings/models.py:27 msgid "Value" msgstr "值" @@ -1146,22 +1336,22 @@ msgstr "空" msgid "favorite" msgstr "收藏夹" -#: assets/models/node.py:452 +#: assets/models/node.py:452 assets/templates/assets/_node_detail_modal.html:39 msgid "Key" msgstr "键" -#: assets/models/user.py:106 +#: assets/models/user.py:107 msgid "Automatic login" msgstr "自动登录" -#: assets/models/user.py:107 +#: assets/models/user.py:108 msgid "Manually login" msgstr "手动登录" -#: assets/models/user.py:111 +#: assets/models/user.py:112 #: assets/templates/assets/_asset_group_bulk_update_modal.html:11 -#: assets/templates/assets/system_user_assets.html:22 -#: assets/templates/assets/system_user_detail.html:22 +#: assets/templates/assets/system_user_assets.html:26 +#: assets/templates/assets/system_user_detail.html:18 #: assets/views/admin_user.py:30 assets/views/admin_user.py:49 #: assets/views/admin_user.py:67 assets/views/admin_user.py:84 #: assets/views/admin_user.py:108 assets/views/asset.py:37 @@ -1170,53 +1360,67 @@ msgstr "手动登录" #: assets/views/cmd_filter.py:31 assets/views/cmd_filter.py:48 #: assets/views/cmd_filter.py:66 assets/views/cmd_filter.py:84 #: assets/views/cmd_filter.py:104 assets/views/cmd_filter.py:138 -#: assets/views/cmd_filter.py:173 assets/views/domain.py:30 -#: assets/views/domain.py:47 assets/views/domain.py:65 -#: assets/views/domain.py:80 assets/views/domain.py:106 -#: assets/views/domain.py:135 assets/views/domain.py:156 +#: assets/views/cmd_filter.py:173 assets/views/domain.py:31 +#: assets/views/domain.py:48 assets/views/domain.py:66 +#: assets/views/domain.py:81 assets/views/domain.py:107 +#: assets/views/domain.py:136 assets/views/domain.py:157 #: assets/views/label.py:27 assets/views/label.py:45 assets/views/label.py:73 +#: assets/views/platform.py:22 assets/views/platform.py:38 +#: assets/views/platform.py:55 assets/views/platform.py:71 #: assets/views/system_user.py:29 assets/views/system_user.py:46 #: assets/views/system_user.py:63 assets/views/system_user.py:79 #: templates/_nav.html:39 xpack/plugins/change_auth_plan/models.py:70 msgid "Assets" msgstr "资产管理" -#: assets/models/user.py:114 assets/templates/assets/_system_user.html:59 -#: assets/templates/assets/system_user_detail.html:122 +#: assets/models/user.py:115 assets/templates/assets/_system_user.html:55 +#: assets/templates/assets/system_user_detail.html:119 #: assets/templates/assets/system_user_update.html:10 msgid "Auto push" msgstr "自动推送" -#: assets/models/user.py:115 assets/templates/assets/system_user_detail.html:74 +#: assets/models/user.py:116 assets/templates/assets/system_user_detail.html:71 msgid "Sudo" msgstr "Sudo" -#: assets/models/user.py:116 assets/templates/assets/system_user_detail.html:79 +#: assets/models/user.py:117 assets/templates/assets/system_user_detail.html:76 msgid "Shell" msgstr "Shell" -#: assets/models/user.py:117 assets/templates/assets/system_user_detail.html:66 -#: assets/templates/assets/system_user_list.html:50 +#: assets/models/user.py:118 assets/templates/assets/system_user_detail.html:63 +#: assets/templates/assets/system_user_list.html:27 msgid "Login mode" msgstr "登录模式" -#: assets/models/user.py:166 assets/templates/assets/user_asset_list.html:79 -#: audits/models.py:21 audits/templates/audits/ftp_log_list.html:52 -#: audits/templates/audits/ftp_log_list.html:75 -#: perms/forms/asset_permission.py:90 perms/forms/remote_app_permission.py:43 -#: perms/models/asset_permission.py:82 perms/models/remote_app_permission.py:16 -#: perms/templates/perms/asset_permission_detail.html:140 -#: perms/templates/perms/asset_permission_list.html:54 -#: perms/templates/perms/asset_permission_list.html:75 -#: perms/templates/perms/asset_permission_list.html:127 -#: perms/templates/perms/remote_app_permission_detail.html:131 +#: assets/models/user.py:179 assets/templates/assets/system_user_list.html:74 +#: assets/templates/assets/user_asset_list.html:79 audits/models.py:21 +#: audits/templates/audits/ftp_log_list.html:53 +#: audits/templates/audits/ftp_log_list.html:76 +#: perms/forms/asset_permission.py:95 perms/forms/remote_app_permission.py:49 +#: perms/models/asset_permission.py:82 +#: perms/models/database_app_permission.py:21 +#: perms/models/remote_app_permission.py:16 +#: perms/templates/perms/asset_permission_asset.html:124 +#: perms/templates/perms/asset_permission_list.html:37 +#: perms/templates/perms/asset_permission_list.html:93 +#: perms/templates/perms/asset_permission_list.html:190 +#: perms/templates/perms/database_app_permission_database_app.html:94 +#: perms/templates/perms/database_app_permission_list.html:18 +#: perms/templates/perms/remote_app_permission_detail.html:126 #: perms/templates/perms/remote_app_permission_list.html:18 #: templates/_nav.html:45 terminal/backends/command/models.py:14 -#: terminal/models.py:158 terminal/templates/terminal/command_list.html:31 +#: terminal/models.py:180 terminal/templates/terminal/command_list.html:31 #: terminal/templates/terminal/command_list.html:67 -#: terminal/templates/terminal/session_list.html:29 -#: terminal/templates/terminal/session_list.html:73 +#: terminal/templates/terminal/session_list.html:27 +#: terminal/templates/terminal/session_list.html:71 #: users/templates/users/_granted_assets.html:32 +#: users/templates/users/user_asset_permission.html:42 +#: users/templates/users/user_asset_permission.html:76 +#: users/templates/users/user_asset_permission.html:159 +#: users/templates/users/user_database_app_permission.html:40 +#: users/templates/users/user_database_app_permission.html:67 +#: users/templates/users/user_remote_app_permission.html:40 +#: users/templates/users/user_remote_app_permission.html:67 #: xpack/plugins/orgs/templates/orgs/org_list.html:20 msgid "System user" msgstr "系统用户" @@ -1231,24 +1435,23 @@ msgid "Unreachable" msgstr "不可达" #: assets/models/utils.py:44 assets/tasks/const.py:85 -#: assets/templates/assets/asset_list.html:99 +#: assets/templates/assets/asset_list.html:27 msgid "Reachable" msgstr "可连接" -#: assets/models/utils.py:45 assets/tasks/const.py:86 -#: authentication/utils.py:13 xpack/plugins/license/models.py:78 +#: assets/models/utils.py:45 assets/tasks/const.py:86 audits/utils.py:30 msgid "Unknown" msgstr "未知" -#: assets/serializers/asset.py:26 +#: assets/serializers/asset.py:23 msgid "Protocol format should {}/{}" msgstr "协议格式 {}/{}" -#: assets/serializers/asset.py:43 +#: assets/serializers/asset.py:40 msgid "Protocol duplicate: {}" msgstr "协议重复: {}" -#: assets/serializers/asset.py:69 assets/serializers/asset.py:143 +#: assets/serializers/asset.py:69 assets/serializers/asset.py:149 #: assets/serializers/asset_user.py:29 #: assets/templates/assets/_asset_user_list.html:23 msgid "Connectivity" @@ -1266,8 +1469,8 @@ msgstr "组织名称" msgid "Backend" msgstr "后端" -#: assets/serializers/asset_user.py:67 users/forms.py:282 -#: users/models/user.py:403 users/templates/users/first_login.html:42 +#: assets/serializers/asset_user.py:67 users/forms/profile.py:148 +#: users/models/user.py:468 users/templates/users/first_login.html:42 #: users/templates/users/user_password_update.html:49 #: users/templates/users/user_profile.html:69 #: users/templates/users/user_profile_update.html:46 @@ -1292,15 +1495,15 @@ msgstr "值" msgid "The same level node name cannot be the same" msgstr "同级别节点名字不能重复" -#: assets/serializers/system_user.py:38 +#: assets/serializers/system_user.py:42 msgid "Login mode display" msgstr "登录模式显示" -#: assets/serializers/system_user.py:82 +#: assets/serializers/system_user.py:77 msgid "* Automatic login mode must fill in the username." msgstr "自动登录模式,必须填写用户名" -#: assets/serializers/system_user.py:93 +#: assets/serializers/system_user.py:88 msgid "Password or private key required" msgstr "密码或密钥密码需要一个" @@ -1322,7 +1525,7 @@ msgstr "测试资产可连接性: {}" #: assets/tasks/asset_user_connectivity.py:27 #: assets/tasks/push_system_user.py:130 -#: xpack/plugins/change_auth_plan/models.py:521 +#: xpack/plugins/change_auth_plan/models.py:527 msgid "The asset {} system platform {} does not support run Ansible tasks" msgstr "资产 {} 系统平台 {} 不支持运行 Ansible 任务" @@ -1342,7 +1545,7 @@ msgstr "更新资产硬件信息" msgid "Update asset hardware info: {}" msgstr "更新资产硬件信息: {}" -#: assets/tasks/gather_asset_users.py:96 +#: assets/tasks/gather_asset_users.py:107 msgid "Gather assets users" msgstr "收集资产上的用户" @@ -1392,15 +1595,6 @@ msgstr "没有匹配到资产,结束任务" msgid "No assets matched related system user protocol, stop task" msgstr "没有匹配到与系统用户协议相关的资产,结束任务" -#: assets/templates/assets/_admin_user_import_modal.html:4 -msgid "Import admin user" -msgstr "导入管理用户" - -#: assets/templates/assets/_admin_user_update_modal.html:4 -#: assets/views/admin_user.py:68 -msgid "Update admin user" -msgstr "更新管理用户" - #: assets/templates/assets/_asset_group_bulk_update_modal.html:5 msgid "Update asset group" msgstr "更新用户组" @@ -1414,8 +1608,9 @@ msgid "Select Asset" msgstr "选择资产" #: assets/templates/assets/_asset_group_bulk_update_modal.html:21 -#: assets/templates/assets/cmd_filter_detail.html:89 -#: assets/templates/assets/cmd_filter_list.html:26 +#: assets/templates/assets/cmd_filter_detail.html:84 +#: assets/templates/assets/cmd_filter_list.html:24 +#: perms/forms/database_app_permission.py:47 msgid "System users" msgstr "系统用户" @@ -1427,10 +1622,6 @@ msgstr "选择系统用户" msgid "Enable-MFA" msgstr "启用MFA" -#: assets/templates/assets/_asset_import_modal.html:4 -msgid "Import assets" -msgstr "导入资产" - #: assets/templates/assets/_asset_list_modal.html:7 assets/views/asset.py:38 #: templates/_nav.html:42 xpack/plugins/change_auth_plan/views.py:118 msgid "Asset list" @@ -1438,18 +1629,14 @@ msgstr "资产列表" #: assets/templates/assets/_asset_list_modal.html:33 #: assets/templates/assets/_node_tree.html:39 -#: ops/templates/ops/command_execution_create.html:70 -#: ops/templates/ops/command_execution_create.html:127 +#: ops/templates/ops/command_execution_create.html:62 +#: ops/templates/ops/command_execution_create.html:112 #: settings/templates/settings/_ldap_list_users_modal.html:41 #: users/templates/users/_granted_assets.html:7 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:66 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:62 msgid "Loading" msgstr "加载中" -#: assets/templates/assets/_asset_update_modal.html:4 -msgid "Update assets" -msgstr "更新资产" - #: assets/templates/assets/_asset_user_auth_update_modal.html:4 msgid "Update asset user auth" msgstr "更新资产用户认证信息" @@ -1460,9 +1647,9 @@ msgid "Please input password" msgstr "请输入密码" #: assets/templates/assets/_asset_user_auth_update_modal.html:68 -#: assets/templates/assets/asset_detail.html:302 -#: users/templates/users/user_detail.html:313 -#: users/templates/users/user_detail.html:340 +#: assets/templates/assets/asset_detail.html:300 +#: users/templates/users/user_detail.html:356 +#: users/templates/users/user_detail.html:383 #: xpack/plugins/interface/views.py:35 msgid "Update successfully!" msgstr "更新成功" @@ -1472,6 +1659,8 @@ msgid "Asset user auth" msgstr "资产用户信息" #: assets/templates/assets/_asset_user_auth_view_modal.html:54 +#: assets/templates/assets/_node_detail_modal.html:56 +#: authentication/templates/authentication/login_wait_confirm.html:114 msgid "Copy success" msgstr "复制成功" @@ -1480,25 +1669,28 @@ msgid "Get auth info error" msgstr "获取认证信息错误" #: assets/templates/assets/_asset_user_auth_view_modal.html:97 +#: assets/templates/assets/_node_detail_modal.html:67 #: assets/templates/assets/_user_asset_detail_modal.html:23 #: authentication/templates/authentication/_access_key_modal.html:142 #: authentication/templates/authentication/_mfa_confirm_modal.html:53 #: settings/templates/settings/_ldap_list_users_modal.html:171 -#: templates/_modal.html:22 +#: templates/_modal.html:22 tickets/models/ticket.py:68 +#: tickets/templates/tickets/ticket_detail.html:103 msgid "Close" msgstr "关闭" #: assets/templates/assets/_asset_user_list.html:24 -#: audits/templates/audits/operate_log_list.html:77 -#: audits/templates/audits/password_change_log_list.html:59 -#: ops/templates/ops/task_adhoc.html:63 +#: audits/templates/audits/operate_log_list.html:75 +#: audits/templates/audits/password_change_log_list.html:57 +#: ops/templates/ops/task_adhoc.html:61 #: terminal/templates/terminal/command_list.html:33 #: terminal/templates/terminal/session_detail.html:50 +#: tickets/templates/tickets/ticket_list.html:37 msgid "Datetime" msgstr "日期" #: assets/templates/assets/_asset_user_list.html:41 -#: assets/templates/assets/asset_list.html:137 +#: assets/templates/assets/asset_list.html:59 msgid "Test datetime: " msgstr "测试日期: " @@ -1507,17 +1699,18 @@ msgid "View" msgstr "查看" #: assets/templates/assets/_asset_user_list.html:76 -#: assets/templates/assets/admin_user_assets.html:61 +#: assets/templates/assets/admin_user_assets.html:56 #: assets/templates/assets/asset_asset_user_list.html:57 -#: assets/templates/assets/asset_detail.html:176 -#: assets/templates/assets/system_user_assets.html:63 -#: assets/templates/assets/system_user_detail.html:151 +#: assets/templates/assets/asset_detail.html:174 +#: assets/templates/assets/system_user_assets.html:67 +#: assets/templates/assets/system_user_detail.html:149 +#: terminal/templates/terminal/base_storage_list.html:72 msgid "Test" msgstr "测试" #: assets/templates/assets/_asset_user_list.html:77 -#: assets/templates/assets/system_user_assets.html:72 -#: assets/templates/assets/system_user_detail.html:142 +#: assets/templates/assets/system_user_assets.html:76 +#: assets/templates/assets/system_user_detail.html:139 msgid "Push" msgstr "推送" @@ -1525,7 +1718,7 @@ msgstr "推送" msgid "Test gateway test connection" msgstr "测试连接网关" -#: assets/templates/assets/_gateway_test_modal.html:10 terminal/models.py:25 +#: assets/templates/assets/_gateway_test_modal.html:10 terminal/models.py:28 msgid "SSH Port" msgstr "SSH端口" @@ -1533,6 +1726,27 @@ msgstr "SSH端口" msgid "If use nat, set the ssh real port" msgstr "如果使用了nat端口映射,请设置为ssh真实监听的端口" +#: assets/templates/assets/_node_detail_modal.html:11 +#: assets/templates/assets/asset_list.html:124 +msgid "Node detail" +msgstr "节点详情" + +#: assets/templates/assets/_node_detail_modal.html:18 +#: audits/templates/audits/login_log_list.html:56 +#: authentication/templates/authentication/_access_key_modal.html:30 +#: ops/templates/ops/adhoc_detail.html:47 +#: ops/templates/ops/adhoc_history_detail.html:47 +#: ops/templates/ops/task_detail.html:54 +#: terminal/templates/terminal/session_list.html:24 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:59 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:57 +msgid "ID" +msgstr "ID" + +#: assets/templates/assets/_node_detail_modal.html:33 +msgid "Full name" +msgstr "全称" + #: assets/templates/assets/_node_tree.html:49 msgid "Add node" msgstr "新建节点" @@ -1557,177 +1771,133 @@ msgstr "存在子节点,不能删除" msgid "Have assets, cancel" msgstr "存在资产,不能删除" -#: assets/templates/assets/_node_tree.html:254 +#: assets/templates/assets/_node_tree.html:255 msgid "Rename success" msgstr "重命名成功" -#: assets/templates/assets/_system_user.html:37 +#: assets/templates/assets/_system_user.html:33 #: assets/templates/assets/asset_create.html:16 -#: assets/templates/assets/gateway_create_update.html:37 -#: perms/templates/perms/asset_permission_create_update.html:38 -#: perms/templates/perms/remote_app_permission_create_update.html:39 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:43 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:27 -#: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:27 +#: assets/templates/assets/gateway_create_update.html:33 +#: perms/templates/perms/asset_permission_create_update.html:48 +#: perms/templates/perms/database_app_permission_create_update.html:37 +#: perms/templates/perms/remote_app_permission_create_update.html:37 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:37 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:23 +#: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:23 msgid "Basic" msgstr "基本" -#: assets/templates/assets/_system_user.html:44 +#: assets/templates/assets/_system_user.html:40 #: assets/templates/assets/asset_create.html:38 -#: assets/templates/assets/gateway_create_update.html:45 +#: assets/templates/assets/gateway_create_update.html:41 #: users/templates/users/_user.html:21 msgid "Auth" msgstr "认证" -#: assets/templates/assets/_system_user.html:48 +#: assets/templates/assets/_system_user.html:44 msgid "Auto generate key" msgstr "自动生成密钥" -#: assets/templates/assets/_system_user.html:69 +#: assets/templates/assets/_system_user.html:65 #: assets/templates/assets/asset_create.html:74 -#: assets/templates/assets/gateway_create_update.html:53 -#: perms/templates/perms/asset_permission_create_update.html:53 -#: perms/templates/perms/remote_app_permission_create_update.html:53 -#: terminal/templates/terminal/terminal_update.html:40 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:67 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:48 -#: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:39 +#: assets/templates/assets/gateway_create_update.html:49 +#: perms/templates/perms/asset_permission_create_update.html:97 +#: perms/templates/perms/database_app_permission_create_update.html:51 +#: perms/templates/perms/remote_app_permission_create_update.html:51 +#: terminal/templates/terminal/terminal_update.html:38 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:61 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:44 +#: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:35 msgid "Other" msgstr "其它" -#: assets/templates/assets/_system_user_import_modal.html:4 -msgid "Import system user" -msgstr "导入系统用户" - -#: assets/templates/assets/_system_user_update_modal.html:4 -#: assets/views/system_user.py:64 -msgid "Update system user" -msgstr "更新系统用户" - #: assets/templates/assets/_user_asset_detail_modal.html:11 #: assets/templates/assets/asset_asset_user_list.html:13 -#: assets/templates/assets/asset_detail.html:20 assets/views/asset.py:200 +#: assets/templates/assets/asset_detail.html:18 assets/views/asset.py:200 msgid "Asset detail" msgstr "资产详情" -#: assets/templates/assets/admin_user_assets.html:21 -#: assets/templates/assets/admin_user_detail.html:21 +#: assets/templates/assets/admin_user_assets.html:16 +#: assets/templates/assets/admin_user_detail.html:16 msgid "Assets list" msgstr "资产列表" -#: assets/templates/assets/admin_user_assets.html:29 -#: perms/templates/perms/asset_permission_asset.html:35 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:31 +#: assets/templates/assets/admin_user_assets.html:24 +#: perms/templates/perms/asset_permission_asset.html:31 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:27 msgid "Asset list of " msgstr "资产列表" -#: assets/templates/assets/admin_user_assets.html:52 -#: assets/templates/assets/system_user_assets.html:54 -#: assets/templates/assets/system_user_detail.html:116 -#: perms/templates/perms/asset_permission_detail.html:114 -#: perms/templates/perms/remote_app_permission_detail.html:106 +#: assets/templates/assets/admin_user_assets.html:47 +#: assets/templates/assets/system_user_assets.html:58 +#: assets/templates/assets/system_user_detail.html:113 +#: perms/templates/perms/asset_permission_detail.html:109 +#: perms/templates/perms/database_app_permission_detail.html:105 +#: perms/templates/perms/remote_app_permission_detail.html:101 msgid "Quick update" msgstr "快速更新" -#: assets/templates/assets/admin_user_assets.html:58 +#: assets/templates/assets/admin_user_assets.html:53 #: assets/templates/assets/asset_asset_user_list.html:54 -#: assets/templates/assets/asset_detail.html:173 +#: assets/templates/assets/asset_detail.html:171 msgid "Test connective" msgstr "测试可连接性" -#: assets/templates/assets/admin_user_detail.html:83 +#: assets/templates/assets/admin_user_detail.html:78 msgid "Replace node assets admin user with this" msgstr "替换资产的管理员" -#: assets/templates/assets/admin_user_detail.html:91 -#: perms/templates/perms/asset_permission_asset.html:103 +#: assets/templates/assets/admin_user_detail.html:86 +#: perms/templates/perms/asset_permission_asset.html:99 #: xpack/plugins/change_auth_plan/forms.py:68 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:99 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:95 #: xpack/plugins/gathered_user/forms.py:36 msgid "Select nodes" msgstr "选择节点" -#: assets/templates/assets/admin_user_detail.html:100 -#: assets/templates/assets/asset_detail.html:202 -#: assets/templates/assets/asset_list.html:423 -#: assets/templates/assets/cmd_filter_detail.html:106 -#: assets/templates/assets/system_user_assets.html:97 -#: assets/templates/assets/system_user_detail.html:182 -#: assets/templates/assets/system_user_list.html:135 +#: assets/templates/assets/admin_user_detail.html:95 +#: assets/templates/assets/asset_detail.html:200 +#: assets/templates/assets/asset_list.html:258 +#: assets/templates/assets/cmd_filter_detail.html:101 +#: assets/templates/assets/system_user_assets.html:101 +#: assets/templates/assets/system_user_detail.html:181 #: authentication/templates/authentication/_mfa_confirm_modal.html:20 -#: settings/templates/settings/terminal_setting.html:168 #: templates/_modal.html:23 terminal/templates/terminal/session_detail.html:112 -#: users/templates/users/user_detail.html:394 -#: users/templates/users/user_detail.html:420 +#: users/templates/users/user_detail.html:264 +#: users/templates/users/user_detail.html:417 #: users/templates/users/user_detail.html:443 -#: users/templates/users/user_detail.html:488 -#: users/templates/users/user_group_create_update.html:32 -#: users/templates/users/user_group_list.html:120 -#: users/templates/users/user_list.html:256 -#: xpack/plugins/cloud/templates/cloud/account_create_update.html:34 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:54 -#: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:45 +#: users/templates/users/user_detail.html:466 +#: users/templates/users/user_detail.html:511 +#: users/templates/users/user_group_create_update.html:28 +#: users/templates/users/user_list.html:184 +#: xpack/plugins/cloud/templates/cloud/account_create_update.html:30 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:50 +#: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:41 #: xpack/plugins/interface/templates/interface/interface.html:103 -#: xpack/plugins/orgs/templates/orgs/org_create_update.html:34 +#: xpack/plugins/orgs/templates/orgs/org_create_update.html:30 msgid "Confirm" msgstr "确认" -#: assets/templates/assets/admin_user_list.html:5 +#: assets/templates/assets/admin_user_list.html:4 msgid "" "Admin users are asset (charged server) on the root, or have NOPASSWD: ALL " "sudo permissions users, " msgstr "" "管理用户是资产(被控服务器)上的 root,或拥有 NOPASSWD: ALL sudo 权限的用户," -#: assets/templates/assets/admin_user_list.html:6 +#: assets/templates/assets/admin_user_list.html:5 msgid "" "Jumpserver users of the system using the user to `push system user`, `get " "assets hardware information`, etc. " msgstr "Jumpserver 使用该用户来 `推送系统用户`、`获取资产硬件信息` 等。" -#: assets/templates/assets/admin_user_list.html:16 -#: assets/templates/assets/asset_list.html:68 -#: assets/templates/assets/system_user_list.html:19 -#: audits/templates/audits/login_log_list.html:91 -#: users/templates/users/user_group_list.html:10 -#: users/templates/users/user_list.html:10 -#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:59 -#: xpack/plugins/vault/templates/vault/vault.html:55 -msgid "Export" -msgstr "导出" - -#: assets/templates/assets/admin_user_list.html:21 -#: assets/templates/assets/asset_list.html:73 -#: assets/templates/assets/system_user_list.html:24 -#: settings/templates/settings/_ldap_list_users_modal.html:172 -#: users/templates/users/user_group_list.html:15 -#: users/templates/users/user_list.html:15 -#: xpack/plugins/license/templates/license/license_detail.html:110 -#: xpack/plugins/vault/templates/vault/vault.html:60 -msgid "Import" -msgstr "导入" - -#: assets/templates/assets/admin_user_list.html:36 +#: assets/templates/assets/admin_user_list.html:13 #: assets/views/admin_user.py:50 msgid "Create admin user" msgstr "创建管理用户" -#: assets/templates/assets/admin_user_list.html:125 -#: assets/templates/assets/admin_user_list.html:156 -#: assets/templates/assets/asset_list.html:304 -#: assets/templates/assets/asset_list.html:341 -#: assets/templates/assets/system_user_list.html:188 -#: assets/templates/assets/system_user_list.html:219 -#: users/templates/users/user_group_list.html:164 -#: users/templates/users/user_group_list.html:195 -#: users/templates/users/user_list.html:165 -#: users/templates/users/user_list.html:197 -#: xpack/plugins/vault/templates/vault/vault.html:224 -msgid "Please select file" -msgstr "选择文件" - #: assets/templates/assets/asset_asset_user_list.html:16 -#: assets/templates/assets/asset_detail.html:23 assets/views/asset.py:55 +#: assets/templates/assets/asset_detail.html:21 assets/views/asset.py:55 msgid "Asset user list" msgstr "资产用户列表" @@ -1736,13 +1906,13 @@ msgid "Asset users of" msgstr "资产用户" #: assets/templates/assets/asset_asset_user_list.html:47 -#: assets/templates/assets/asset_detail.html:142 +#: assets/templates/assets/asset_detail.html:140 #: terminal/templates/terminal/session_detail.html:85 -#: users/templates/users/user_detail.html:140 +#: users/templates/users/user_detail.html:126 #: users/templates/users/user_profile.html:150 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:128 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:132 -#: xpack/plugins/license/templates/license/license_detail.html:102 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:126 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:129 +#: xpack/plugins/license/templates/license/license_detail.html:80 msgid "Quick modify" msgstr "快速修改" @@ -1756,43 +1926,46 @@ msgstr "选择需要修改属性" msgid "Select all" msgstr "全选" -#: assets/templates/assets/asset_detail.html:90 +#: assets/templates/assets/asset_detail.html:88 msgid "CPU" msgstr "CPU" -#: assets/templates/assets/asset_detail.html:98 +#: assets/templates/assets/asset_detail.html:96 msgid "Disk" msgstr "硬盘" -#: assets/templates/assets/asset_detail.html:126 -#: users/templates/users/user_detail.html:115 +#: assets/templates/assets/asset_detail.html:124 +#: users/templates/users/user_detail.html:101 #: users/templates/users/user_profile.html:106 msgid "Date joined" msgstr "创建日期" -#: assets/templates/assets/asset_detail.html:148 authentication/models.py:15 +#: assets/templates/assets/asset_detail.html:146 authentication/models.py:19 #: authentication/templates/authentication/_access_key_modal.html:32 #: perms/models/base.py:51 -#: perms/templates/perms/asset_permission_create_update.html:55 -#: perms/templates/perms/asset_permission_detail.html:120 -#: perms/templates/perms/remote_app_permission_create_update.html:55 -#: perms/templates/perms/remote_app_permission_detail.html:112 -#: terminal/templates/terminal/terminal_list.html:34 +#: perms/templates/perms/asset_permission_create_update.html:99 +#: perms/templates/perms/asset_permission_detail.html:115 +#: perms/templates/perms/database_app_permission_create_update.html:53 +#: perms/templates/perms/database_app_permission_detail.html:111 +#: perms/templates/perms/remote_app_permission_create_update.html:53 +#: perms/templates/perms/remote_app_permission_detail.html:107 +#: terminal/templates/terminal/terminal_list.html:35 #: users/templates/users/_select_user_modal.html:18 -#: users/templates/users/user_detail.html:146 +#: users/templates/users/user_detail.html:132 #: users/templates/users/user_profile.html:63 msgid "Active" msgstr "激活中" -#: assets/templates/assets/asset_detail.html:165 +#: assets/templates/assets/asset_detail.html:163 msgid "Refresh hardware" msgstr "更新硬件信息" -#: assets/templates/assets/asset_detail.html:168 +#: assets/templates/assets/asset_detail.html:166 +#: authentication/templates/authentication/login_wait_confirm.html:42 msgid "Refresh" msgstr "刷新" -#: assets/templates/assets/asset_list.html:8 +#: assets/templates/assets/asset_list.html:6 msgid "" "The left side is the asset tree, right click to create, delete, and change " "the tree node, authorization asset is also organized as a node, and the " @@ -1801,99 +1974,94 @@ msgstr "" "左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的," "右侧是属于该节点下的资产" -#: assets/templates/assets/asset_list.html:61 assets/views/asset.py:104 +#: assets/templates/assets/asset_list.html:10 assets/views/asset.py:104 msgid "Create asset" msgstr "创建资产" -#: assets/templates/assets/asset_list.html:98 +#: assets/templates/assets/asset_list.html:26 msgid "Hardware" msgstr "硬件" -#: assets/templates/assets/asset_list.html:109 -#: users/templates/users/user_list.html:50 +#: assets/templates/assets/asset_list.html:37 +#: users/templates/users/user_list.html:30 msgid "Delete selected" msgstr "批量删除" -#: assets/templates/assets/asset_list.html:110 -#: users/templates/users/user_list.html:51 +#: assets/templates/assets/asset_list.html:38 +#: users/templates/users/user_list.html:34 msgid "Update selected" msgstr "批量更新" -#: assets/templates/assets/asset_list.html:111 +#: assets/templates/assets/asset_list.html:39 msgid "Remove from this node" msgstr "从节点移除" -#: assets/templates/assets/asset_list.html:112 -#: users/templates/users/user_list.html:52 +#: assets/templates/assets/asset_list.html:40 +#: users/templates/users/user_list.html:35 msgid "Deactive selected" msgstr "禁用所选" -#: assets/templates/assets/asset_list.html:113 -#: users/templates/users/user_list.html:53 +#: assets/templates/assets/asset_list.html:41 +#: users/templates/users/user_list.html:36 msgid "Active selected" msgstr "激活所选" -#: assets/templates/assets/asset_list.html:193 +#: assets/templates/assets/asset_list.html:115 msgid "Add assets to node" msgstr "添加资产到节点" -#: assets/templates/assets/asset_list.html:194 +#: assets/templates/assets/asset_list.html:116 msgid "Move assets to node" msgstr "移动资产到节点" -#: assets/templates/assets/asset_list.html:196 +#: assets/templates/assets/asset_list.html:118 msgid "Refresh node hardware info" msgstr "更新节点资产硬件信息" -#: assets/templates/assets/asset_list.html:197 +#: assets/templates/assets/asset_list.html:119 msgid "Test node connective" msgstr "测试节点资产可连接性" -#: assets/templates/assets/asset_list.html:199 +#: assets/templates/assets/asset_list.html:121 msgid "Display only current node assets" msgstr "仅显示当前节点资产" -#: assets/templates/assets/asset_list.html:200 +#: assets/templates/assets/asset_list.html:122 msgid "Displays all child node assets" msgstr "显示所有子节点资产" -#: assets/templates/assets/asset_list.html:417 -#: assets/templates/assets/system_user_list.html:129 -#: users/templates/users/user_detail.html:388 -#: users/templates/users/user_detail.html:414 -#: users/templates/users/user_detail.html:482 -#: users/templates/users/user_group_list.html:114 -#: users/templates/users/user_list.html:250 +#: assets/templates/assets/asset_list.html:252 +#: users/templates/users/user_detail.html:411 +#: users/templates/users/user_detail.html:437 +#: users/templates/users/user_detail.html:505 +#: users/templates/users/user_list.html:178 #: xpack/plugins/interface/templates/interface/interface.html:97 msgid "Are you sure?" msgstr "你确认吗?" -#: assets/templates/assets/asset_list.html:418 +#: assets/templates/assets/asset_list.html:253 msgid "This will delete the selected assets !!!" msgstr "删除选择资产" -#: assets/templates/assets/asset_list.html:421 -#: assets/templates/assets/system_user_list.html:133 -#: settings/templates/settings/terminal_setting.html:166 -#: users/templates/users/user_detail.html:392 -#: users/templates/users/user_detail.html:418 -#: users/templates/users/user_detail.html:486 -#: users/templates/users/user_group_list.html:118 -#: users/templates/users/user_list.html:254 +#: assets/templates/assets/asset_list.html:256 +#: users/templates/users/user_detail.html:415 +#: users/templates/users/user_detail.html:441 +#: users/templates/users/user_detail.html:509 +#: users/templates/users/user_list.html:182 #: xpack/plugins/interface/templates/interface/interface.html:101 msgid "Cancel" msgstr "取消" -#: assets/templates/assets/asset_list.html:432 +#: assets/templates/assets/asset_list.html:267 msgid "Asset Deleted." msgstr "已被删除" -#: assets/templates/assets/asset_list.html:433 -#: assets/templates/assets/asset_list.html:441 +#: assets/templates/assets/asset_list.html:268 +#: assets/templates/assets/asset_list.html:276 msgid "Asset Delete" msgstr "删除" -#: assets/templates/assets/asset_list.html:440 +#: assets/templates/assets/asset_list.html:275 msgid "Asset Deleting failed." msgstr "删除失败" @@ -1901,57 +2069,57 @@ msgstr "删除失败" msgid "Configuration" msgstr "配置" -#: assets/templates/assets/cmd_filter_detail.html:25 -#: assets/templates/assets/cmd_filter_list.html:25 -#: assets/templates/assets/cmd_filter_rule_list.html:23 +#: assets/templates/assets/cmd_filter_detail.html:20 +#: assets/templates/assets/cmd_filter_list.html:23 +#: assets/templates/assets/cmd_filter_rule_list.html:18 msgid "Rules" msgstr "规则" -#: assets/templates/assets/cmd_filter_detail.html:97 +#: assets/templates/assets/cmd_filter_detail.html:92 msgid "Binding to system user" msgstr "绑定到系统用户" -#: assets/templates/assets/cmd_filter_list.html:6 +#: assets/templates/assets/cmd_filter_list.html:5 msgid "" "System user bound some command filter, each command filter has some rules," msgstr "系统用户可以绑定一些命令过滤器,一个过滤器可以定义一些规则" -#: assets/templates/assets/cmd_filter_list.html:7 +#: assets/templates/assets/cmd_filter_list.html:6 msgid "When user login asset with this system user, then run a command," msgstr "当用户使用这个系统用户登录资产,然后执行一个命令" -#: assets/templates/assets/cmd_filter_list.html:8 +#: assets/templates/assets/cmd_filter_list.html:7 msgid "The command will be filter by rules, higher priority rule run first," msgstr "这个命令需要被绑定过滤器的所有规则匹配,高优先级先被匹配," -#: assets/templates/assets/cmd_filter_list.html:9 +#: assets/templates/assets/cmd_filter_list.html:8 msgid "" "When a rule matched, if rule action is allow, then allow command execute," msgstr "当一个规则匹配到了,如果规则的动作是允许,这个命令会被放行," -#: assets/templates/assets/cmd_filter_list.html:10 +#: assets/templates/assets/cmd_filter_list.html:9 msgid "else if action is deny, then command with be deny," msgstr "如果规则的动作是禁止,命令将会被禁止执行," -#: assets/templates/assets/cmd_filter_list.html:11 +#: assets/templates/assets/cmd_filter_list.html:10 msgid "else match next rule, if none matched, allowed" msgstr "否则就匹配下一个规则,如果最后没有匹配到规则,则允许执行" -#: assets/templates/assets/cmd_filter_list.html:16 +#: assets/templates/assets/cmd_filter_list.html:14 #: assets/views/cmd_filter.py:49 msgid "Create command filter" msgstr "创建命令过滤器" -#: assets/templates/assets/cmd_filter_rule_list.html:33 +#: assets/templates/assets/cmd_filter_rule_list.html:28 #: assets/views/cmd_filter.py:105 msgid "Command filter rule list" msgstr "命令过滤器规则列表" -#: assets/templates/assets/cmd_filter_rule_list.html:50 +#: assets/templates/assets/cmd_filter_rule_list.html:45 msgid "Create rule" msgstr "创建规则" -#: assets/templates/assets/cmd_filter_rule_list.html:61 +#: assets/templates/assets/cmd_filter_rule_list.html:56 msgid "Strategy" msgstr "策略" @@ -1965,27 +2133,27 @@ msgstr "确认删除" msgid "Are you sure delete" msgstr "您确定删除吗?" -#: assets/templates/assets/domain_gateway_list.html:37 +#: assets/templates/assets/domain_gateway_list.html:32 msgid "Gateway list" msgstr "网关列表" -#: assets/templates/assets/domain_gateway_list.html:56 -#: assets/views/domain.py:136 +#: assets/templates/assets/domain_gateway_list.html:51 +#: assets/views/domain.py:137 msgid "Create gateway" msgstr "创建网关" -#: assets/templates/assets/domain_gateway_list.html:99 -#: assets/templates/assets/domain_gateway_list.html:101 -#: settings/templates/settings/email_setting.html:64 -#: settings/templates/settings/ldap_setting.html:65 +#: assets/templates/assets/domain_gateway_list.html:94 +#: assets/templates/assets/domain_gateway_list.html:96 +#: settings/templates/settings/email_setting.html:45 +#: settings/templates/settings/ldap_setting.html:46 msgid "Test connection" msgstr "测试连接" -#: assets/templates/assets/domain_gateway_list.html:141 +#: assets/templates/assets/domain_gateway_list.html:136 msgid "Can be connected" msgstr "可连接" -#: assets/templates/assets/domain_list.html:9 +#: assets/templates/assets/domain_list.html:6 msgid "" "The domain function is added to address the fact that some environments " "(such as the hybrid cloud) cannot be connected directly by jumping on the " @@ -1994,11 +2162,11 @@ msgstr "" "网域功能是为了解决部分环境(如:混合云)无法直接连接而新增的功能,原理是通过" "网关服务器进行跳转登录。" -#: assets/templates/assets/domain_list.html:11 +#: assets/templates/assets/domain_list.html:8 msgid "JMS => Domain gateway => Target assets" msgstr "JMS => 网域网关 => 目标资产" -#: assets/templates/assets/domain_list.html:17 assets/views/domain.py:48 +#: assets/templates/assets/domain_list.html:13 assets/views/domain.py:49 msgid "Create domain" msgstr "创建网域" @@ -2006,37 +2174,41 @@ msgstr "创建网域" msgid "Create label" msgstr "创建标签" -#: assets/templates/assets/system_user_assets.html:31 +#: assets/templates/assets/platform_list.html:8 assets/views/platform.py:39 +msgid "Create platform" +msgstr "创建系统平台" + +#: assets/templates/assets/system_user_assets.html:35 msgid "Assets of " msgstr "资产" -#: assets/templates/assets/system_user_assets.html:60 -#: assets/templates/assets/system_user_detail.html:148 +#: assets/templates/assets/system_user_assets.html:64 +#: assets/templates/assets/system_user_detail.html:146 msgid "Test assets connective" msgstr "测试资产可连接性" -#: assets/templates/assets/system_user_assets.html:69 -#: assets/templates/assets/system_user_detail.html:139 +#: assets/templates/assets/system_user_assets.html:73 +#: assets/templates/assets/system_user_detail.html:136 msgid "Push system user now" msgstr "立刻推送系统" -#: assets/templates/assets/system_user_assets.html:91 +#: assets/templates/assets/system_user_assets.html:95 msgid "Add to node" msgstr "添加到节点" -#: assets/templates/assets/system_user_detail.html:85 +#: assets/templates/assets/system_user_detail.html:82 msgid "Home" msgstr "家目录" -#: assets/templates/assets/system_user_detail.html:91 +#: assets/templates/assets/system_user_detail.html:88 msgid "Uid" msgstr "Uid" -#: assets/templates/assets/system_user_detail.html:173 +#: assets/templates/assets/system_user_detail.html:172 msgid "Binding command filters" msgstr "绑定命令过滤器" -#: assets/templates/assets/system_user_list.html:6 +#: assets/templates/assets/system_user_list.html:5 msgid "" "System user is Jumpserver jump login assets used by the users, can be " "understood as the user login assets, such as web, sa, the dba (` ssh " @@ -2047,7 +2219,7 @@ msgstr "" "web,sa,dba(`ssh web@some-host`),而不是使用某个用户的用户名跳转登录服务器" "(`ssh xiaoming@some-host`);" -#: assets/templates/assets/system_user_list.html:7 +#: assets/templates/assets/system_user_list.html:6 msgid "" "In simple terms, users log into Jumpserver using their own username, and " "Jumpserver uses system users to log into assets. " @@ -2055,7 +2227,7 @@ msgstr "" "简单来说是用户使用自己的用户名登录 Jumpserver,Jumpserver 使用系统用户登录资" "产。" -#: assets/templates/assets/system_user_list.html:8 +#: assets/templates/assets/system_user_list.html:7 msgid "" "When system users are created, if you choose auto push Jumpserver to use " "Ansible push system users into the asset, if the asset (Switch) does not " @@ -2064,32 +2236,19 @@ msgstr "" "系统用户创建时,如果选择了自动推送,Jumpserver 会使用 Ansible 自动推送系统用" "户到资产中,如果资产(交换机)不支持 Ansible,请手动填写账号密码。" -#: assets/templates/assets/system_user_list.html:39 +#: assets/templates/assets/system_user_list.html:16 #: assets/views/system_user.py:47 msgid "Create system user" msgstr "创建系统用户" -#: assets/templates/assets/system_user_list.html:130 -msgid "This will delete the selected System Users !!!" -msgstr "删除选择系统用户" - -#: assets/templates/assets/system_user_list.html:139 -msgid "System Users Deleted." -msgstr "已被删除" - -#: assets/templates/assets/system_user_list.html:140 -#: assets/templates/assets/system_user_list.html:145 -msgid "System Users Delete" -msgstr "删除系统用户" - -#: assets/templates/assets/system_user_list.html:144 -msgid "System Users Deleting failed." -msgstr "系统用户删除失败" - #: assets/views/admin_user.py:31 msgid "Admin user list" msgstr "管理用户列表" +#: assets/views/admin_user.py:68 +msgid "Update admin user" +msgstr "更新管理用户" + #: assets/views/admin_user.py:85 assets/views/admin_user.py:109 msgid "Admin user detail" msgstr "管理用户详情" @@ -2130,23 +2289,23 @@ msgstr "创建命令过滤器规则" msgid "Update command filter rule" msgstr "更新命令过滤器规则" -#: assets/views/domain.py:31 templates/_nav.html:43 +#: assets/views/domain.py:32 templates/_nav.html:43 msgid "Domain list" msgstr "网域列表" -#: assets/views/domain.py:66 +#: assets/views/domain.py:67 msgid "Update domain" msgstr "更新网域" -#: assets/views/domain.py:81 +#: assets/views/domain.py:82 msgid "Domain detail" msgstr "网域详情" -#: assets/views/domain.py:107 +#: assets/views/domain.py:108 msgid "Domain gateway list" msgstr "域网关列表" -#: assets/views/domain.py:157 +#: assets/views/domain.py:158 msgid "Update gateway" msgstr "创建网关" @@ -2162,10 +2321,26 @@ msgstr "提示: 请避免使用内部预留标签名: {}" msgid "Update label" msgstr "更新标签" +#: assets/views/platform.py:23 templates/_nav.html:49 +msgid "Platform list" +msgstr "平台列表" + +#: assets/views/platform.py:56 +msgid "Update platform" +msgstr "更新系统平台" + +#: assets/views/platform.py:72 +msgid "Platform detail" +msgstr "平台详情" + #: assets/views/system_user.py:30 msgid "System user list" msgstr "系统用户列表" +#: assets/views/system_user.py:64 +msgid "Update system user" +msgstr "更新系统用户" + #: assets/views/system_user.py:80 msgid "System user detail" msgstr "系统用户详情" @@ -2179,29 +2354,29 @@ msgid "System user asset" msgstr "系统用户资产" #: audits/models.py:19 audits/models.py:42 audits/models.py:53 -#: audits/templates/audits/ftp_log_list.html:76 -#: audits/templates/audits/operate_log_list.html:76 -#: audits/templates/audits/password_change_log_list.html:58 -#: terminal/models.py:160 terminal/templates/terminal/session_list.html:30 -#: terminal/templates/terminal/session_list.html:74 +#: audits/templates/audits/ftp_log_list.html:77 +#: audits/templates/audits/operate_log_list.html:74 +#: audits/templates/audits/password_change_log_list.html:56 +#: terminal/models.py:183 terminal/templates/terminal/session_list.html:28 +#: terminal/templates/terminal/session_list.html:72 #: terminal/templates/terminal/terminal_detail.html:47 msgid "Remote addr" msgstr "远端地址" -#: audits/models.py:22 audits/templates/audits/ftp_log_list.html:77 +#: audits/models.py:22 audits/templates/audits/ftp_log_list.html:78 msgid "Operate" msgstr "操作" -#: audits/models.py:23 audits/templates/audits/ftp_log_list.html:59 -#: audits/templates/audits/ftp_log_list.html:78 +#: audits/models.py:23 audits/templates/audits/ftp_log_list.html:60 +#: audits/templates/audits/ftp_log_list.html:79 msgid "Filename" msgstr "文件名" #: audits/models.py:24 audits/models.py:77 -#: audits/templates/audits/ftp_log_list.html:79 -#: ops/templates/ops/command_execution_list.html:68 -#: ops/templates/ops/task_list.html:15 -#: users/templates/users/user_detail.html:464 +#: audits/templates/audits/ftp_log_list.html:80 +#: ops/templates/ops/command_execution_list.html:71 +#: ops/templates/ops/task_list.html:14 +#: users/templates/users/user_detail.html:487 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:14 #: xpack/plugins/cloud/api.py:61 msgid "Success" @@ -2209,29 +2384,29 @@ msgstr "成功" #: audits/models.py:33 #: authentication/templates/authentication/_access_key_modal.html:22 -#: xpack/plugins/vault/templates/vault/vault.html:46 +#: xpack/plugins/vault/templates/vault/vault.html:8 msgid "Create" msgstr "创建" -#: audits/models.py:40 audits/templates/audits/operate_log_list.html:55 -#: audits/templates/audits/operate_log_list.html:74 +#: audits/models.py:40 audits/templates/audits/operate_log_list.html:53 +#: audits/templates/audits/operate_log_list.html:72 msgid "Resource Type" msgstr "资源类型" -#: audits/models.py:41 audits/templates/audits/operate_log_list.html:75 +#: audits/models.py:41 audits/templates/audits/operate_log_list.html:73 msgid "Resource" msgstr "资源" -#: audits/models.py:52 audits/templates/audits/password_change_log_list.html:57 +#: audits/models.py:52 audits/templates/audits/password_change_log_list.html:55 msgid "Change by" msgstr "修改者" -#: audits/models.py:71 users/templates/users/user_detail.html:98 +#: audits/models.py:71 users/templates/users/user_detail.html:84 msgid "Disabled" msgstr "禁用" -#: audits/models.py:72 settings/models.py:33 -#: users/templates/users/user_detail.html:96 +#: audits/models.py:72 settings/models.py:30 +#: users/templates/users/user_detail.html:82 msgid "Enabled" msgstr "启用" @@ -2239,8 +2414,8 @@ msgstr "启用" msgid "-" msgstr "" -#: audits/models.py:78 xpack/plugins/cloud/models.py:264 -#: xpack/plugins/cloud/models.py:287 +#: audits/models.py:78 xpack/plugins/cloud/models.py:263 +#: xpack/plugins/cloud/models.py:286 msgid "Failed" msgstr "失败" @@ -2262,23 +2437,26 @@ msgstr "Agent" #: audits/models.py:86 audits/templates/audits/login_log_list.html:62 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 -#: users/forms.py:194 users/models/user.py:395 +#: users/forms/profile.py:52 users/models/user.py:460 #: users/templates/users/first_login.html:45 msgid "MFA" msgstr "MFA" #: audits/models.py:87 audits/templates/audits/login_log_list.html:63 -#: xpack/plugins/change_auth_plan/models.py:416 +#: xpack/plugins/change_auth_plan/models.py:422 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:15 -#: xpack/plugins/cloud/models.py:278 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:69 +#: xpack/plugins/cloud/models.py:277 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:64 msgid "Reason" msgstr "原因" #: audits/models.py:88 audits/templates/audits/login_log_list.html:64 -#: xpack/plugins/cloud/models.py:275 xpack/plugins/cloud/models.py:310 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:70 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:65 +#: tickets/templates/tickets/ticket_detail.html:34 +#: tickets/templates/tickets/ticket_list.html:36 +#: tickets/templates/tickets/ticket_list.html:104 +#: xpack/plugins/cloud/models.py:274 xpack/plugins/cloud/models.py:309 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:65 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:62 msgid "Status" msgstr "状态" @@ -2286,83 +2464,83 @@ msgstr "状态" msgid "Date login" msgstr "登录日期" -#: audits/templates/audits/ftp_log_list.html:80 -#: ops/templates/ops/adhoc_history.html:52 -#: ops/templates/ops/adhoc_history_detail.html:61 -#: ops/templates/ops/command_execution_list.html:69 -#: ops/templates/ops/task_history.html:58 perms/models/base.py:52 -#: perms/templates/perms/asset_permission_detail.html:86 -#: perms/templates/perms/remote_app_permission_detail.html:78 -#: terminal/models.py:167 terminal/templates/terminal/session_list.html:34 +#: audits/templates/audits/ftp_log_list.html:81 +#: ops/templates/ops/adhoc_history.html:50 +#: ops/templates/ops/adhoc_history_detail.html:59 +#: ops/templates/ops/command_execution_list.html:72 +#: ops/templates/ops/task_history.html:56 perms/models/base.py:52 +#: perms/templates/perms/asset_permission_detail.html:81 +#: perms/templates/perms/database_app_permission_detail.html:77 +#: perms/templates/perms/remote_app_permission_detail.html:73 +#: terminal/models.py:189 terminal/templates/terminal/session_list.html:32 #: xpack/plugins/change_auth_plan/models.py:249 -#: xpack/plugins/change_auth_plan/models.py:419 +#: xpack/plugins/change_auth_plan/models.py:425 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:59 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:17 -#: xpack/plugins/gathered_user/models.py:143 +#: xpack/plugins/gathered_user/models.py:140 msgid "Date start" msgstr "开始日期" #: audits/templates/audits/login_log_list.html:34 -#: perms/templates/perms/asset_permission_user.html:88 -#: perms/templates/perms/remote_app_permission_user.html:87 +#: perms/templates/perms/asset_permission_user.html:74 +#: perms/templates/perms/database_app_permission_user.html:74 +#: perms/templates/perms/remote_app_permission_user.html:83 msgid "Select user" msgstr "选择用户" #: audits/templates/audits/login_log_list.html:41 #: audits/templates/audits/login_log_list.html:46 -#: audits/templates/audits/operate_log_list.html:64 -#: audits/templates/audits/password_change_log_list.html:48 -#: ops/templates/ops/command_execution_list.html:46 -#: ops/templates/ops/command_execution_list.html:51 -#: templates/_base_list.html:41 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:52 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:48 +#: audits/templates/audits/operate_log_list.html:62 +#: audits/templates/audits/password_change_log_list.html:46 +#: ops/templates/ops/command_execution_list.html:49 +#: ops/templates/ops/command_execution_list.html:54 +#: templates/_base_list.html:37 templates/_user_profile.html:23 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:47 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:45 msgid "Search" msgstr "搜索" -#: audits/templates/audits/login_log_list.html:56 -#: authentication/templates/authentication/_access_key_modal.html:30 -#: ops/templates/ops/adhoc_detail.html:49 -#: ops/templates/ops/adhoc_history_detail.html:49 -#: ops/templates/ops/task_detail.html:56 -#: terminal/templates/terminal/session_list.html:26 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:64 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:60 -msgid "ID" -msgstr "ID" - #: audits/templates/audits/login_log_list.html:59 msgid "UA" msgstr "Agent" -#: audits/templates/audits/login_log_list.html:61 +#: audits/templates/audits/login_log_list.html:61 authentication/models.py:63 msgid "City" msgstr "城市" #: audits/templates/audits/login_log_list.html:65 #: authentication/templates/authentication/_access_key_modal.html:33 -#: ops/templates/ops/task_list.html:16 +#: ops/templates/ops/task_list.html:15 msgid "Date" msgstr "日期" +#: audits/templates/audits/login_log_list.html:91 +#: templates/_csv_import_export.html:8 +msgid "Export" +msgstr "导出" + +#: audits/templates/audits/operate_log_list.html:70 +msgid "Handlers" +msgstr "操作者" + #: audits/views.py:86 audits/views.py:130 audits/views.py:167 -#: audits/views.py:212 audits/views.py:244 templates/_nav.html:129 +#: audits/views.py:212 audits/views.py:244 templates/_nav.html:146 msgid "Audits" msgstr "日志审计" -#: audits/views.py:87 templates/_nav.html:133 +#: audits/views.py:87 templates/_nav.html:150 msgid "FTP log" msgstr "FTP日志" -#: audits/views.py:131 templates/_nav.html:134 +#: audits/views.py:131 templates/_nav.html:151 msgid "Operate log" msgstr "操作日志" -#: audits/views.py:168 templates/_nav.html:135 +#: audits/views.py:168 templates/_nav.html:152 msgid "Password change log" msgstr "改密日志" -#: audits/views.py:213 templates/_nav.html:132 +#: audits/views.py:213 templates/_nav.html:149 msgid "Login log" msgstr "登录日志" @@ -2370,28 +2548,6 @@ msgstr "登录日志" msgid "Command execution log" msgstr "命令执行" -#: authentication/api/auth.py:61 authentication/api/token.py:45 -#: authentication/templates/authentication/login.html:52 -#: authentication/templates/authentication/new_login.html:77 -msgid "Log in frequently and try again later" -msgstr "登录频繁, 稍后重试" - -#: authentication/api/auth.py:86 -msgid "Please carry seed value and conduct MFA secondary certification" -msgstr "请携带seed值, 进行MFA二次认证" - -#: authentication/api/auth.py:176 -msgid "Please verify the user name and password first" -msgstr "请先进行用户名和密码验证" - -#: authentication/api/auth.py:181 -msgid "MFA certification failed" -msgstr "MFA认证失败" - -#: authentication/api/token.py:80 -msgid "MFA required" -msgstr "" - #: authentication/backends/api.py:53 msgid "Invalid signature header. No credentials provided." msgstr "" @@ -2443,56 +2599,88 @@ msgstr "" msgid "Invalid token or cache refreshed." msgstr "" -#: authentication/const.py:6 +#: authentication/errors.py:20 msgid "Username/password check failed" msgstr "用户名/密码 校验失败" -#: authentication/const.py:7 +#: authentication/errors.py:21 msgid "MFA authentication failed" msgstr "MFA 认证失败" -#: authentication/const.py:8 +#: authentication/errors.py:22 msgid "Username does not exist" msgstr "用户名不存在" -#: authentication/const.py:9 +#: authentication/errors.py:23 msgid "Password expired" -msgstr "密码过期" +msgstr "密码已过期" -#: authentication/const.py:10 +#: authentication/errors.py:24 msgid "Disabled or expired" msgstr "禁用或失效" -#: authentication/forms.py:21 -msgid "" -"The username or password you entered is incorrect, please enter it again." -msgstr "您输入的用户名或密码不正确,请重新输入。" - -#: authentication/forms.py:24 +#: authentication/errors.py:25 msgid "This account is inactive." -msgstr "此账户无效" +msgstr "此账户已禁用" -#: authentication/forms.py:26 +#: authentication/errors.py:35 +msgid "No session found, check your cookie" +msgstr "会话已变更,刷新页面" + +#: authentication/errors.py:37 #, python-brace-format msgid "" +"The username or password you entered is incorrect, please enter it again. " "You can also try {times_try} times (The account will be temporarily locked " "for {block_time} minutes)" -msgstr "您还可以尝试 {times_try} 次(账号将被临时锁定 {block_time} 分钟)" +msgstr "" +"您输入的用户名或密码不正确,请重新输入。 您还可以尝试 {times_try} 次(账号将" +"被临时 锁定 {block_time} 分钟)" -#: authentication/forms.py:30 +#: authentication/errors.py:43 msgid "" "The account has been locked (please contact admin to unlock it or try again " "after {} minutes)" msgstr "账号已被锁定(请联系管理员解锁 或 {}分钟后重试)" -#: authentication/forms.py:66 users/forms.py:22 +#: authentication/errors.py:46 users/views/profile.py:202 +#: users/views/profile.py:238 +msgid "MFA code invalid, or ntp sync server time" +msgstr "MFA验证码不正确,或者服务器端时间不对" + +#: authentication/errors.py:48 +msgid "MFA required" +msgstr "" + +#: authentication/errors.py:49 +msgid "Login confirm required" +msgstr "需要登录复核" + +#: authentication/errors.py:50 +msgid "Wait login confirm ticket for accept" +msgstr "等待登录复核处理" + +#: authentication/errors.py:51 +msgid "Login confirm ticket was {}" +msgstr "登录复核 {}" + +#: authentication/forms.py:29 users/forms/user.py:199 msgid "MFA code" msgstr "MFA 验证码" -#: authentication/models.py:35 +#: authentication/models.py:39 msgid "Private Token" msgstr "ssh密钥" +#: authentication/models.py:44 users/templates/users/user_detail.html:258 +msgid "Reviewers" +msgstr "审批人" + +#: authentication/models.py:53 tickets/models/ticket.py:25 +#: users/templates/users/user_detail.html:250 +msgid "Login confirm" +msgstr "登录复核" + #: authentication/templates/authentication/_access_key_modal.html:6 msgid "API key list" msgstr "API Key列表" @@ -2515,14 +2703,14 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/models/user.py:330 users/templates/users/user_profile.html:94 +#: users/models/user.py:360 users/templates/users/user_profile.html:94 #: users/templates/users/user_profile.html:163 #: users/templates/users/user_profile.html:166 msgid "Disable" msgstr "禁用" #: authentication/templates/authentication/_access_key_modal.html:67 -#: users/models/user.py:331 users/templates/users/user_profile.html:92 +#: users/models/user.py:361 users/templates/users/user_profile.html:92 #: users/templates/users/user_profile.html:170 msgid "Enable" msgstr "启用" @@ -2541,7 +2729,6 @@ msgstr "代码错误" #: authentication/templates/authentication/login.html:27 #: authentication/templates/authentication/login_otp.html:27 -#: users/templates/users/reset_password.html:25 #: xpack/plugins/interface/models.py:36 msgid "Welcome to the Jumpserver open source fortress" msgstr "欢迎使用Jumpserver开源堡垒机" @@ -2582,68 +2769,63 @@ msgstr "" msgid "Changes the world, starting with a little bit." msgstr "改变世界,从一点点开始。" -#: authentication/templates/authentication/login.html:46 -#: authentication/templates/authentication/login.html:73 -#: authentication/templates/authentication/new_login.html:101 +#: authentication/templates/authentication/login.html:45 +#: authentication/templates/authentication/login.html:76 +#: authentication/templates/authentication/xpack_login.html:112 #: templates/_header_bar.html:83 msgid "Login" msgstr "登录" #: authentication/templates/authentication/login.html:54 -#: authentication/templates/authentication/new_login.html:80 -msgid "The user password has expired" -msgstr "用户密码已过期" - -#: authentication/templates/authentication/login.html:57 -#: authentication/templates/authentication/new_login.html:83 +#: authentication/templates/authentication/xpack_login.html:87 msgid "Captcha invalid" msgstr "验证码错误" -#: authentication/templates/authentication/login.html:84 -#: authentication/templates/authentication/new_login.html:105 -#: users/templates/users/forgot_password.html:10 -#: users/templates/users/forgot_password.html:25 +#: authentication/templates/authentication/login.html:87 +#: authentication/templates/authentication/xpack_login.html:116 +#: users/templates/users/forgot_password.html:12 +#: users/templates/users/forgot_password.html:13 msgid "Forgot password" msgstr "忘记密码" -#: authentication/templates/authentication/login.html:91 +#: authentication/templates/authentication/login.html:94 msgid "More login options" msgstr "更多登录方式" -#: authentication/templates/authentication/login.html:95 +#: authentication/templates/authentication/login.html:98 msgid "Keycloak" msgstr "" #: authentication/templates/authentication/login_otp.html:46 -#: users/templates/users/user_detail.html:91 +#: users/templates/users/user_detail.html:77 #: users/templates/users/user_profile.html:87 msgid "MFA certification" msgstr "MFA认证" #: authentication/templates/authentication/login_otp.html:51 -#: users/templates/users/user_otp_authentication.html:11 +#: users/templates/users/user_disable_mfa.html:11 msgid "" "The account protection has been opened, please complete the following " "operations according to the prompts" msgstr "账号保护已开启,请根据提示完成以下操作" #: authentication/templates/authentication/login_otp.html:55 -#: users/templates/users/user_otp_authentication.html:13 +#: users/templates/users/user_disable_mfa.html:13 msgid "Open Authenticator and enter the 6-bit dynamic code" msgstr "请打开手机Google Authenticator应用,输入6位动态码" #: authentication/templates/authentication/login_otp.html:65 -#: users/templates/users/user_otp_authentication.html:23 -#: users/templates/users/user_otp_enable_bind.html:26 +#: users/templates/users/user_disable_mfa.html:23 +#: users/templates/users/user_otp_enable_bind.html:25 msgid "Six figures" msgstr "6位数字" #: authentication/templates/authentication/login_otp.html:67 #: users/templates/users/first_login.html:108 -#: users/templates/users/user_otp_authentication.html:26 -#: users/templates/users/user_otp_enable_bind.html:29 -#: users/templates/users/user_otp_enable_install_app.html:26 -#: users/templates/users/user_password_authentication.html:21 +#: users/templates/users/user_disable_mfa.html:26 +#: users/templates/users/user_otp_enable_bind.html:28 +#: users/templates/users/user_otp_enable_install_app.html:25 +#: users/templates/users/user_password_check.html:16 msgid "Next" msgstr "下一步" @@ -2651,24 +2833,40 @@ msgstr "下一步" msgid "Can't provide security? Please contact the administrator!" msgstr "如果不能提供MFA验证码,请联系管理员!" -#: authentication/templates/authentication/new_login.html:67 +#: authentication/templates/authentication/login_wait_confirm.html:47 +msgid "Copy link" +msgstr "复制链接" + +#: authentication/templates/authentication/login_wait_confirm.html:52 +#: templates/flash_message_standalone.html:34 +msgid "Return" +msgstr "返回" + +#: authentication/templates/authentication/xpack_login.html:74 msgid "Welcome back, please enter username and password to login" msgstr "欢迎回来,请输入用户名和密码登录" -#: authentication/views/login.py:81 +#: authentication/views/login.py:71 msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: authentication/views/login.py:174 users/views/user.py:393 -#: users/views/user.py:418 -msgid "MFA code invalid, or ntp sync server time" -msgstr "MFA验证码不正确,或者服务器端时间不对" +#: authentication/views/login.py:159 +msgid "" +"Wait for {} confirm, You also can copy link to her/him
    \n" +" Don't close this page" +msgstr "" +"等待 {} 确认, 你也可以复制链接发给他/她
    \n" +" 不要关闭本页面" -#: authentication/views/login.py:205 +#: authentication/views/login.py:164 +msgid "No ticket found" +msgstr "没有发现工单" + +#: authentication/views/login.py:187 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:206 +#: authentication/views/login.py:188 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" @@ -2682,11 +2880,11 @@ msgstr "%(name)s 创建成功" msgid "%(name)s was updated successfully" msgstr "%(name)s 更新成功" -#: common/fields/form.py:34 +#: common/fields/form.py:33 msgid "Not a valid json" msgstr "不是合法json" -#: common/fields/form.py:36 +#: common/fields/form.py:35 msgid "Not a string type" msgstr "不是字符类型" @@ -2714,7 +2912,7 @@ msgstr "" msgid "Marshal data to text field" msgstr "" -#: common/fields/model.py:123 +#: common/fields/model.py:133 msgid "Encrypt field using Secret Key" msgstr "" @@ -2726,6 +2924,10 @@ msgstr "" msgid "discard time" msgstr "" +#: common/utils/ipip/utils.py:15 +msgid "Invalid ip" +msgstr "无效IP" + #: common/validators.py:11 msgid "Special char not allowed" msgstr "不能包含特殊字符" @@ -2734,15 +2936,15 @@ msgstr "不能包含特殊字符" msgid "This field must be unique." msgstr "字段必须唯一" -#: jumpserver/celery_flower.py:21 +#: jumpserver/views/celery_flower.py:23 msgid "

    Flow service unavailable, check it

    " msgstr "" -#: jumpserver/views.py:184 templates/_nav.html:7 +#: jumpserver/views/index.py:178 templates/_nav.html:7 msgid "Dashboard" msgstr "仪表盘" -#: jumpserver/views.py:193 +#: jumpserver/views/other.py:25 msgid "" "
    Luna is a separately deployed program, you need to deploy Luna, koko, " "configure nginx for url distribution,
    If you see this page, " @@ -2751,242 +2953,262 @@ msgstr "" "
    Luna是单独部署的一个程序,你需要部署luna,koko,
    如果你看到了" "这个页面,证明你访问的不是nginx监听的端口,祝你好运
    " -#: jumpserver/views.py:233 +#: jumpserver/views/other.py:65 msgid "Websocket server run on port: {}, you should proxy it on nginx" +msgstr "Websocket 服务运行在端口: {}, 请检查nginx是否代理是否设置" + +#: jumpserver/views/other.py:73 +msgid "" +"
    Koko is a separately deployed program, you need to deploy Koko, " +"configure nginx for url distribution,
    If you see this page, " +"prove that you are not accessing the nginx listening port. Good luck.
    " msgstr "" +"
    Koko是单独部署的一个程序,你需要部署Koko, 并确保nginx配置转发,
    如果你看到了这个页面,证明你访问的不是nginx监听的端口,祝你好运" -#: ops/api/celery.py:54 +#: ops/api/celery.py:57 msgid "Waiting task start" msgstr "等待任务开始" #: ops/api/command.py:35 msgid "Not has host {} permission" -msgstr "" +msgstr "没有该主机 {} 权限" -#: ops/models/adhoc.py:38 +#: ops/models/adhoc.py:41 msgid "Interval" msgstr "间隔" -#: ops/models/adhoc.py:38 settings/forms.py:162 +#: ops/models/adhoc.py:41 settings/forms/terminal.py:34 msgid "Units: seconds" msgstr "单位: 秒" -#: ops/models/adhoc.py:39 +#: ops/models/adhoc.py:42 msgid "Crontab" msgstr "Crontab" -#: ops/models/adhoc.py:39 +#: ops/models/adhoc.py:42 msgid "5 * * * *" msgstr "5 * * * *" -#: ops/models/adhoc.py:41 +#: ops/models/adhoc.py:44 msgid "Callback" msgstr "回调" -#: ops/models/adhoc.py:183 ops/templates/ops/adhoc_detail.html:114 +#: ops/models/adhoc.py:182 ops/templates/ops/adhoc_detail.html:112 msgid "Tasks" msgstr "任务" -#: ops/models/adhoc.py:184 ops/templates/ops/adhoc_detail.html:57 -#: ops/templates/ops/task_adhoc.html:60 +#: ops/models/adhoc.py:183 ops/templates/ops/adhoc_detail.html:55 +#: ops/templates/ops/task_adhoc.html:58 msgid "Pattern" msgstr "模式" -#: ops/models/adhoc.py:185 ops/templates/ops/adhoc_detail.html:61 +#: ops/models/adhoc.py:184 ops/templates/ops/adhoc_detail.html:59 msgid "Options" msgstr "选项" -#: ops/models/adhoc.py:186 ops/templates/ops/adhoc_detail.html:53 -#: ops/templates/ops/command_execution_list.html:62 -#: ops/templates/ops/task_adhoc.html:59 ops/templates/ops/task_list.html:14 -#: settings/templates/settings/command_storage_create.html:49 -msgid "Hosts" -msgstr "主机" - -#: ops/models/adhoc.py:187 -#: settings/templates/settings/replay_storage_create.html:52 -#: templates/index.html:91 -msgid "Host" -msgstr "主机" - -#: ops/models/adhoc.py:188 +#: ops/models/adhoc.py:186 msgid "Run as admin" msgstr "再次执行" -#: ops/models/adhoc.py:190 ops/templates/ops/adhoc_detail.html:82 -#: ops/templates/ops/task_adhoc.html:62 +#: ops/models/adhoc.py:188 ops/templates/ops/adhoc_detail.html:80 +#: ops/templates/ops/task_adhoc.html:60 msgid "Become" msgstr "Become" -#: ops/models/adhoc.py:191 users/templates/users/user_group_detail.html:59 -#: xpack/plugins/cloud/templates/cloud/account_detail.html:62 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:56 +#: ops/models/adhoc.py:189 users/templates/users/user_group_detail.html:54 +#: xpack/plugins/cloud/templates/cloud/account_detail.html:59 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:51 msgid "Create by" msgstr "创建者" -#: ops/models/adhoc.py:252 -msgid "{} Start task: {}" -msgstr "{} 任务开始: {}" +#: ops/models/adhoc.py:270 +msgid "Task display" +msgstr "任务展示" -#: ops/models/adhoc.py:264 -msgid "{} Task finish" -msgstr "{} 任务结束" +#: ops/models/adhoc.py:271 +msgid "Host amount" +msgstr "主机数量" -#: ops/models/adhoc.py:356 +#: ops/models/adhoc.py:273 msgid "Start time" msgstr "开始时间" -#: ops/models/adhoc.py:357 +#: ops/models/adhoc.py:274 msgid "End time" msgstr "完成时间" -#: ops/models/adhoc.py:358 ops/templates/ops/adhoc_history.html:57 -#: ops/templates/ops/task_history.html:63 ops/templates/ops/task_list.html:17 +#: ops/models/adhoc.py:275 ops/templates/ops/adhoc_history.html:55 +#: ops/templates/ops/task_history.html:61 ops/templates/ops/task_list.html:16 #: xpack/plugins/change_auth_plan/models.py:252 -#: xpack/plugins/change_auth_plan/models.py:422 +#: xpack/plugins/change_auth_plan/models.py:428 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:58 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:16 -#: xpack/plugins/gathered_user/models.py:146 +#: xpack/plugins/gathered_user/models.py:143 msgid "Time" msgstr "时间" -#: ops/models/adhoc.py:359 ops/templates/ops/adhoc_detail.html:106 -#: ops/templates/ops/adhoc_history.html:55 -#: ops/templates/ops/adhoc_history_detail.html:69 -#: ops/templates/ops/task_detail.html:84 ops/templates/ops/task_history.html:61 +#: ops/models/adhoc.py:276 ops/templates/ops/adhoc_detail.html:104 +#: ops/templates/ops/adhoc_history.html:53 +#: ops/templates/ops/adhoc_history_detail.html:67 +#: ops/templates/ops/task_detail.html:82 ops/templates/ops/task_history.html:59 msgid "Is finished" msgstr "是否完成" -#: ops/models/adhoc.py:360 ops/templates/ops/adhoc_history.html:56 -#: ops/templates/ops/task_history.html:62 +#: ops/models/adhoc.py:277 ops/templates/ops/adhoc_history.html:54 +#: ops/templates/ops/task_history.html:60 msgid "Is success" msgstr "是否成功" -#: ops/models/adhoc.py:361 +#: ops/models/adhoc.py:278 msgid "Adhoc raw result" msgstr "结果" -#: ops/models/adhoc.py:362 +#: ops/models/adhoc.py:279 msgid "Adhoc result summary" msgstr "汇总" -#: ops/models/command.py:22 +#: ops/models/adhoc.py:323 +msgid "{} Start task: {}" +msgstr "{} 任务开始: {}" + +#: ops/models/adhoc.py:332 +msgid "{} Task finish" +msgstr "{} 任务结束" + +#: ops/models/command.py:23 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:56 -#: xpack/plugins/cloud/models.py:273 +#: xpack/plugins/cloud/models.py:272 msgid "Result" msgstr "结果" -#: ops/models/command.py:57 +#: ops/models/command.py:58 msgid "Task start" msgstr "任务开始" -#: ops/models/command.py:71 +#: ops/models/command.py:80 msgid "Command `{}` is forbidden ........" msgstr "命令 `{}` 不允许被执行 ......." -#: ops/models/command.py:77 +#: ops/models/command.py:86 msgid "Task end" msgstr "任务结束" -#: ops/templates/ops/adhoc_detail.html:19 -#: ops/templates/ops/adhoc_history.html:19 +#: ops/tasks.py:63 +msgid "Clean task history period" +msgstr "定期清除任务历史" + +#: ops/tasks.py:76 +msgid "Clean celery log period" +msgstr "" + +#: ops/templates/ops/adhoc_detail.html:17 +#: ops/templates/ops/adhoc_history.html:17 msgid "Version detail" msgstr "版本详情" -#: ops/templates/ops/adhoc_detail.html:22 -#: ops/templates/ops/adhoc_history.html:22 ops/views/adhoc.py:111 +#: ops/templates/ops/adhoc_detail.html:20 +#: ops/templates/ops/adhoc_history.html:20 ops/views/adhoc.py:111 msgid "Version run history" msgstr "执行历史" -#: ops/templates/ops/adhoc_detail.html:72 -#: ops/templates/ops/adhoc_detail.html:77 +#: ops/templates/ops/adhoc_detail.html:51 #: ops/templates/ops/command_execution_list.html:65 -#: ops/templates/ops/task_adhoc.html:61 +#: ops/templates/ops/task_adhoc.html:57 ops/templates/ops/task_list.html:13 +#: terminal/forms/storage.py:158 +msgid "Hosts" +msgstr "主机" + +#: ops/templates/ops/adhoc_detail.html:70 +#: ops/templates/ops/adhoc_detail.html:75 +#: ops/templates/ops/command_execution_list.html:68 +#: ops/templates/ops/task_adhoc.html:59 msgid "Run as" msgstr "运行用户" -#: ops/templates/ops/adhoc_detail.html:94 ops/templates/ops/task_list.html:12 +#: ops/templates/ops/adhoc_detail.html:92 ops/templates/ops/task_list.html:12 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:18 #: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:19 msgid "Run times" msgstr "执行次数" -#: ops/templates/ops/adhoc_detail.html:98 ops/templates/ops/task_detail.html:76 +#: ops/templates/ops/adhoc_detail.html:96 ops/templates/ops/task_detail.html:74 msgid "Last run" msgstr "最后运行" -#: ops/templates/ops/adhoc_detail.html:102 -#: ops/templates/ops/adhoc_history_detail.html:65 -#: ops/templates/ops/task_detail.html:80 +#: ops/templates/ops/adhoc_detail.html:100 +#: ops/templates/ops/adhoc_history_detail.html:63 +#: ops/templates/ops/task_detail.html:78 msgid "Time delta" msgstr "运行时间" -#: ops/templates/ops/adhoc_detail.html:110 -#: ops/templates/ops/adhoc_history_detail.html:73 -#: ops/templates/ops/task_detail.html:88 +#: ops/templates/ops/adhoc_detail.html:108 +#: ops/templates/ops/adhoc_history_detail.html:71 +#: ops/templates/ops/task_detail.html:92 msgid "Is success " msgstr "成功" -#: ops/templates/ops/adhoc_detail.html:131 -#: ops/templates/ops/task_detail.html:109 +#: ops/templates/ops/adhoc_detail.html:129 +#: ops/templates/ops/task_detail.html:119 msgid "Last run failed hosts" msgstr "最后运行失败主机" -#: ops/templates/ops/adhoc_detail.html:151 -#: ops/templates/ops/adhoc_detail.html:176 -#: ops/templates/ops/task_detail.html:129 -#: ops/templates/ops/task_detail.html:154 +#: ops/templates/ops/adhoc_detail.html:149 +#: ops/templates/ops/adhoc_detail.html:174 +#: ops/templates/ops/task_detail.html:139 +#: ops/templates/ops/task_detail.html:164 msgid "No hosts" msgstr "没有主机" -#: ops/templates/ops/adhoc_detail.html:161 -#: ops/templates/ops/task_detail.html:139 +#: ops/templates/ops/adhoc_detail.html:159 +#: ops/templates/ops/task_detail.html:149 msgid "Last run success hosts" msgstr "最后运行成功主机" -#: ops/templates/ops/adhoc_history.html:30 -#: ops/templates/ops/task_history.html:36 +#: ops/templates/ops/adhoc_history.html:28 +#: ops/templates/ops/task_history.html:34 msgid "History of " msgstr "执行历史" -#: ops/templates/ops/adhoc_history.html:53 -#: ops/templates/ops/task_history.html:59 +#: ops/templates/ops/adhoc_history.html:51 +#: ops/templates/ops/task_history.html:57 msgid "F/S/T" msgstr "失败/成功/总" -#: ops/templates/ops/adhoc_history.html:54 -#: ops/templates/ops/task_history.html:60 +#: ops/templates/ops/adhoc_history.html:52 +#: ops/templates/ops/task_history.html:58 msgid "Ratio" msgstr "比例" -#: ops/templates/ops/adhoc_history_detail.html:19 ops/views/adhoc.py:125 +#: ops/templates/ops/adhoc_history_detail.html:17 ops/views/adhoc.py:125 msgid "Run history detail" msgstr "执行历史详情" -#: ops/templates/ops/adhoc_history_detail.html:22 -#: ops/templates/ops/command_execution_list.html:66 +#: ops/templates/ops/adhoc_history_detail.html:20 +#: ops/templates/ops/command_execution_list.html:69 #: terminal/backends/command/models.py:16 msgid "Output" msgstr "输出" -#: ops/templates/ops/adhoc_history_detail.html:30 +#: ops/templates/ops/adhoc_history_detail.html:28 msgid "History detail of" msgstr "执行历史详情" -#: ops/templates/ops/adhoc_history_detail.html:53 +#: ops/templates/ops/adhoc_history_detail.html:51 msgid "Task name" msgstr "任务名称" -#: ops/templates/ops/adhoc_history_detail.html:84 +#: ops/templates/ops/adhoc_history_detail.html:82 msgid "Failed assets" msgstr "失败资产" -#: ops/templates/ops/adhoc_history_detail.html:104 -#: ops/templates/ops/adhoc_history_detail.html:129 +#: ops/templates/ops/adhoc_history_detail.html:102 +#: ops/templates/ops/adhoc_history_detail.html:127 msgid "No assets" msgstr "没有资产" -#: ops/templates/ops/adhoc_history_detail.html:114 +#: ops/templates/ops/adhoc_history_detail.html:112 msgid "Success assets" msgstr "成功资产" @@ -2994,106 +3216,110 @@ msgstr "成功资产" msgid "Task log" msgstr "任务列表" -#: ops/templates/ops/command_execution_create.html:109 +#: ops/templates/ops/command_execution_create.html:93 #: terminal/templates/terminal/session_detail.html:95 #: terminal/templates/terminal/session_detail.html:104 msgid "Go" msgstr "" -#: ops/templates/ops/command_execution_create.html:194 +#: ops/templates/ops/command_execution_create.html:159 +msgid "Asset configuration does not include the SSH protocol" +msgstr "资产配置不包含 SSH 协议" + +#: ops/templates/ops/command_execution_create.html:183 msgid "Selected assets" msgstr "已选择资产" -#: ops/templates/ops/command_execution_create.html:197 +#: ops/templates/ops/command_execution_create.html:186 msgid "In total" msgstr "总共" -#: ops/templates/ops/command_execution_create.html:234 +#: ops/templates/ops/command_execution_create.html:223 msgid "" "Select the left asset, select the running system user, execute command in " "batch" msgstr "选择左侧资产, 选择运行的系统用户,批量执行命令" -#: ops/templates/ops/command_execution_create.html:278 +#: ops/templates/ops/command_execution_create.html:267 msgid "Unselected assets" msgstr "没有选中资产" -#: ops/templates/ops/command_execution_create.html:282 +#: ops/templates/ops/command_execution_create.html:271 msgid "No input command" msgstr "没有输入命令" -#: ops/templates/ops/command_execution_create.html:286 +#: ops/templates/ops/command_execution_create.html:275 msgid "No system user was selected" msgstr "没有选择系统用户" -#: ops/templates/ops/command_execution_create.html:296 +#: ops/templates/ops/command_execution_create.html:285 msgid "Pending" msgstr "等待" -#: ops/templates/ops/command_execution_list.html:67 +#: ops/templates/ops/command_execution_list.html:70 msgid "Finished" msgstr "结束" -#: ops/templates/ops/task_adhoc.html:19 ops/templates/ops/task_detail.html:20 -#: ops/templates/ops/task_history.html:19 ops/views/adhoc.py:55 +#: ops/templates/ops/task_adhoc.html:17 ops/templates/ops/task_detail.html:18 +#: ops/templates/ops/task_history.html:17 ops/views/adhoc.py:55 msgid "Task detail" msgstr "任务详情" -#: ops/templates/ops/task_adhoc.html:22 ops/templates/ops/task_detail.html:23 -#: ops/templates/ops/task_history.html:22 ops/views/adhoc.py:69 +#: ops/templates/ops/task_adhoc.html:20 ops/templates/ops/task_detail.html:21 +#: ops/templates/ops/task_history.html:20 ops/views/adhoc.py:69 msgid "Task versions" msgstr "任务各版本" -#: ops/templates/ops/task_adhoc.html:25 ops/templates/ops/task_detail.html:26 -#: ops/templates/ops/task_history.html:25 +#: ops/templates/ops/task_adhoc.html:23 ops/templates/ops/task_detail.html:24 +#: ops/templates/ops/task_history.html:23 msgid "Run history" msgstr "执行历史" -#: ops/templates/ops/task_adhoc.html:28 ops/templates/ops/task_detail.html:29 -#: ops/templates/ops/task_history.html:28 +#: ops/templates/ops/task_adhoc.html:26 ops/templates/ops/task_detail.html:27 +#: ops/templates/ops/task_history.html:26 msgid "Last run output" msgstr "输出" -#: ops/templates/ops/task_adhoc.html:36 +#: ops/templates/ops/task_adhoc.html:34 msgid "Versions of " msgstr "版本" -#: ops/templates/ops/task_detail.html:68 +#: ops/templates/ops/task_detail.html:66 msgid "Total versions" msgstr "版本数量" -#: ops/templates/ops/task_detail.html:92 +#: ops/templates/ops/task_detail.html:102 msgid "Contents" msgstr "内容" -#: ops/templates/ops/task_list.html:13 -msgid "Versions" -msgstr "版本" - -#: ops/templates/ops/task_list.html:68 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:137 +#: ops/templates/ops/task_list.html:73 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:135 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:54 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:141 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:138 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:55 #: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:44 msgid "Run" msgstr "执行" -#: ops/templates/ops/task_list.html:108 +#: ops/templates/ops/task_list.html:114 msgid "Task start: " msgstr "任务开始: " -#: ops/utils.py:51 +#: ops/utils.py:53 msgid "Update task content: {}" msgstr "更新任务内容: {}" +#: ops/utils.py:63 +msgid "Disk used more than 80%: {} => {}" +msgstr "" + #: ops/views/adhoc.py:31 ops/views/adhoc.py:54 ops/views/adhoc.py:68 #: ops/views/adhoc.py:82 ops/views/adhoc.py:96 ops/views/adhoc.py:110 #: ops/views/adhoc.py:124 ops/views/command.py:48 ops/views/command.py:79 msgid "Ops" msgstr "作业中心" -#: ops/views/adhoc.py:32 templates/_nav.html:115 +#: ops/views/adhoc.py:32 templates/_nav.html:124 #: xpack/plugins/gathered_user/views.py:35 msgid "Task list" msgstr "任务列表" @@ -3106,11 +3332,11 @@ msgstr "执行历史" msgid "Command execution list" msgstr "命令执行列表" -#: ops/views/command.py:80 templates/_nav_user.html:26 +#: ops/views/command.py:80 templates/_nav_user.html:31 msgid "Command execution" msgstr "命令执行" -#: orgs/mixins/models.py:58 orgs/mixins/serializers.py:26 orgs/models.py:31 +#: orgs/mixins/models.py:44 orgs/mixins/serializers.py:26 orgs/models.py:31 msgid "Organization" msgstr "组织" @@ -3122,34 +3348,42 @@ msgstr "未分组" msgid "Empty" msgstr "空" -#: perms/forms/asset_permission.py:24 +#: perms/forms/asset_permission.py:23 msgid "" "Tips: The RDP protocol does not support separate controls for uploading or " "downloading files" msgstr "提示:RDP 协议不支持单独控制上传或下载文件" -#: perms/forms/asset_permission.py:81 perms/forms/remote_app_permission.py:37 -#: perms/models/base.py:50 perms/templates/perms/asset_permission_list.html:51 -#: perms/templates/perms/asset_permission_list.html:71 -#: perms/templates/perms/asset_permission_list.html:118 +#: perms/forms/asset_permission.py:86 perms/forms/database_app_permission.py:41 +#: perms/forms/remote_app_permission.py:43 perms/models/base.py:50 +#: perms/templates/perms/asset_permission_list.html:34 +#: perms/templates/perms/asset_permission_list.html:84 +#: perms/templates/perms/asset_permission_list.html:186 +#: perms/templates/perms/database_app_permission_list.html:16 #: perms/templates/perms/remote_app_permission_list.html:16 -#: templates/_nav.html:21 users/forms.py:313 users/models/group.py:26 -#: users/models/user.py:379 users/templates/users/_select_user_modal.html:16 -#: users/templates/users/user_detail.html:218 -#: users/templates/users/user_list.html:38 +#: templates/_nav.html:21 users/forms/user.py:168 users/models/group.py:31 +#: users/models/user.py:444 users/templates/users/_select_user_modal.html:16 +#: users/templates/users/user_asset_permission.html:39 +#: users/templates/users/user_asset_permission.html:67 +#: users/templates/users/user_database_app_permission.html:38 +#: users/templates/users/user_database_app_permission.html:61 +#: users/templates/users/user_detail.html:209 +#: users/templates/users/user_list.html:17 +#: users/templates/users/user_remote_app_permission.html:38 +#: users/templates/users/user_remote_app_permission.html:61 #: xpack/plugins/orgs/templates/orgs/org_list.html:16 msgid "User group" msgstr "用户组" -#: perms/forms/asset_permission.py:103 perms/forms/remote_app_permission.py:53 +#: perms/forms/asset_permission.py:108 msgid "User or group at least one required" msgstr "用户和用户组至少选一个" -#: perms/forms/asset_permission.py:112 +#: perms/forms/asset_permission.py:117 msgid "Asset or group at least one required" msgstr "资产和节点至少选一个" -#: perms/models/asset_permission.py:31 settings/forms.py:147 +#: perms/models/asset_permission.py:31 settings/forms/terminal.py:19 msgid "All" msgstr "全部" @@ -3169,178 +3403,253 @@ msgstr "上传下载" msgid "Actions" msgstr "动作" -#: perms/models/asset_permission.py:87 templates/_nav.html:72 +#: perms/models/asset_permission.py:87 templates/_nav.html:78 +#: tickets/templates/tickets/ticket_list.html:22 +#: users/templates/users/_user_detail_nav_header.html:31 +#: users/views/user.py:221 msgid "Asset permission" msgstr "资产授权" #: perms/models/base.py:53 -#: perms/templates/perms/asset_permission_detail.html:90 -#: perms/templates/perms/remote_app_permission_detail.html:82 -#: users/models/user.py:411 users/templates/users/user_detail.html:107 +#: perms/templates/perms/asset_permission_detail.html:85 +#: perms/templates/perms/database_app_permission_detail.html:81 +#: perms/templates/perms/remote_app_permission_detail.html:77 +#: users/models/user.py:476 users/templates/users/user_detail.html:93 #: users/templates/users/user_profile.html:120 msgid "Date expired" msgstr "失效日期" +#: perms/models/database_app_permission.py:26 +#: users/templates/users/_user_detail_nav_header.html:61 +#: users/views/user.py:277 +msgid "DatabaseApp permission" +msgstr "数据库应用授权" + #: perms/models/remote_app_permission.py:20 +#: users/templates/users/_user_detail_nav_header.html:47 +#: users/views/user.py:249 msgid "RemoteApp permission" msgstr "远程应用授权" -#: perms/templates/perms/asset_permission_asset.html:22 -#: perms/templates/perms/asset_permission_detail.html:22 -#: perms/templates/perms/asset_permission_user.html:22 -#: perms/templates/perms/remote_app_permission_detail.html:22 -#: perms/templates/perms/remote_app_permission_remote_app.html:21 -#: perms/templates/perms/remote_app_permission_user.html:21 +#: perms/templates/perms/asset_permission_asset.html:18 +#: perms/templates/perms/asset_permission_detail.html:17 +#: perms/templates/perms/asset_permission_user.html:18 +#: perms/templates/perms/database_app_permission_database_app.html:18 +#: perms/templates/perms/database_app_permission_detail.html:17 +#: perms/templates/perms/database_app_permission_user.html:18 +#: perms/templates/perms/remote_app_permission_detail.html:17 +#: perms/templates/perms/remote_app_permission_remote_app.html:17 +#: perms/templates/perms/remote_app_permission_user.html:17 msgid "Users and user groups" msgstr "用户或用户组" -#: perms/templates/perms/asset_permission_asset.html:27 -#: perms/templates/perms/asset_permission_detail.html:27 -#: perms/templates/perms/asset_permission_user.html:27 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:20 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:23 +#: perms/templates/perms/asset_permission_asset.html:23 +#: perms/templates/perms/asset_permission_detail.html:22 +#: perms/templates/perms/asset_permission_user.html:23 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:16 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:21 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:20 msgid "Assets and node" msgstr "资产或节点" -#: perms/templates/perms/asset_permission_asset.html:70 +#: perms/templates/perms/asset_permission_asset.html:66 msgid "Add asset to this permission" msgstr "添加资产" -#: perms/templates/perms/asset_permission_asset.html:84 -#: perms/templates/perms/asset_permission_detail.html:157 -#: perms/templates/perms/asset_permission_user.html:97 -#: perms/templates/perms/asset_permission_user.html:125 -#: perms/templates/perms/remote_app_permission_detail.html:148 -#: perms/templates/perms/remote_app_permission_remote_app.html:96 -#: perms/templates/perms/remote_app_permission_user.html:96 -#: perms/templates/perms/remote_app_permission_user.html:124 -#: settings/templates/settings/terminal_setting.html:98 -#: settings/templates/settings/terminal_setting.html:120 -#: users/templates/users/user_group_detail.html:92 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:80 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:93 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:130 +#: perms/templates/perms/asset_permission_asset.html:80 +#: perms/templates/perms/asset_permission_asset.html:141 +#: perms/templates/perms/asset_permission_user.html:80 +#: perms/templates/perms/asset_permission_user.html:108 +#: perms/templates/perms/database_app_permission_database_app.html:83 +#: perms/templates/perms/database_app_permission_database_app.html:111 +#: perms/templates/perms/database_app_permission_user.html:80 +#: perms/templates/perms/database_app_permission_user.html:108 +#: perms/templates/perms/remote_app_permission_detail.html:143 +#: perms/templates/perms/remote_app_permission_remote_app.html:92 +#: perms/templates/perms/remote_app_permission_user.html:92 +#: perms/templates/perms/remote_app_permission_user.html:120 +#: users/templates/users/user_group_detail.html:87 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:76 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:88 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:125 msgid "Add" msgstr "添加" -#: perms/templates/perms/asset_permission_asset.html:95 +#: perms/templates/perms/asset_permission_asset.html:91 msgid "Add node to this permission" msgstr "添加节点" -#: perms/templates/perms/asset_permission_asset.html:109 -#: users/templates/users/user_detail.html:235 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:105 +#: perms/templates/perms/asset_permission_asset.html:105 +#: users/templates/users/user_detail.html:226 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:101 msgid "Join" msgstr "加入" -#: perms/templates/perms/asset_permission_create_update.html:61 -#: perms/templates/perms/remote_app_permission_create_update.html:61 +#: perms/templates/perms/asset_permission_asset.html:132 +#: perms/templates/perms/database_app_permission_database_app.html:102 +#: perms/templates/perms/remote_app_permission_detail.html:134 +msgid "Select system users" +msgstr "选择系统用户" + +#: perms/templates/perms/asset_permission_create_update.html:105 +#: perms/templates/perms/database_app_permission_create_update.html:59 +#: perms/templates/perms/remote_app_permission_create_update.html:59 msgid "Validity period" msgstr "有效期" -#: perms/templates/perms/asset_permission_detail.html:66 -#: perms/templates/perms/remote_app_permission_detail.html:66 -#: xpack/plugins/license/templates/license/license_detail.html:76 +#: perms/templates/perms/asset_permission_detail.html:61 +#: perms/templates/perms/database_app_permission_detail.html:61 +#: perms/templates/perms/remote_app_permission_detail.html:61 msgid "User count" msgstr "用户数量" -#: perms/templates/perms/asset_permission_detail.html:70 -#: perms/templates/perms/remote_app_permission_detail.html:70 +#: perms/templates/perms/asset_permission_detail.html:65 +#: perms/templates/perms/database_app_permission_detail.html:65 +#: perms/templates/perms/remote_app_permission_detail.html:65 msgid "User group count" msgstr "用户组数量" -#: perms/templates/perms/asset_permission_detail.html:74 -#: xpack/plugins/license/templates/license/license_detail.html:72 +#: perms/templates/perms/asset_permission_detail.html:69 +#: xpack/plugins/license/templates/license/license_detail.html:63 msgid "Asset count" msgstr "资产数量" -#: perms/templates/perms/asset_permission_detail.html:78 +#: perms/templates/perms/asset_permission_detail.html:73 msgid "Node count" msgstr "节点数量" -#: perms/templates/perms/asset_permission_detail.html:82 +#: perms/templates/perms/asset_permission_detail.html:77 +#: perms/templates/perms/database_app_permission_detail.html:73 msgid "System user count" msgstr "系统用户数量" -#: perms/templates/perms/asset_permission_detail.html:148 -#: perms/templates/perms/remote_app_permission_detail.html:139 -msgid "Select system users" -msgstr "选择系统用户" - -#: perms/templates/perms/asset_permission_list.html:38 +#: perms/templates/perms/asset_permission_list.html:21 +#: perms/templates/perms/database_app_permission_list.html:6 #: perms/templates/perms/remote_app_permission_list.html:6 msgid "Create permission" msgstr "创建授权规则" -#: perms/templates/perms/asset_permission_list.html:42 +#: perms/templates/perms/asset_permission_list.html:25 msgid "Refresh permission cache" msgstr "刷新授权缓存" -#: perms/templates/perms/asset_permission_list.html:55 -#: perms/templates/perms/asset_permission_list.html:69 +#: perms/templates/perms/asset_permission_list.html:38 +#: perms/templates/perms/asset_permission_list.html:184 +#: perms/templates/perms/database_app_permission_list.html:19 #: perms/templates/perms/remote_app_permission_list.html:19 -#: users/templates/users/user_list.html:40 xpack/plugins/cloud/models.py:74 -#: xpack/plugins/cloud/templates/cloud/account_detail.html:58 +#: users/templates/users/user_asset_permission.html:43 +#: users/templates/users/user_asset_permission.html:155 +#: users/templates/users/user_database_app_permission.html:41 +#: users/templates/users/user_list.html:19 +#: users/templates/users/user_remote_app_permission.html:41 +#: xpack/plugins/cloud/models.py:73 +#: xpack/plugins/cloud/templates/cloud/account_detail.html:55 #: xpack/plugins/cloud/templates/cloud/account_list.html:14 msgid "Validity" msgstr "有效" -#: perms/templates/perms/asset_permission_list.html:244 +#: perms/templates/perms/asset_permission_list.html:191 +#: users/templates/users/user_asset_permission.html:160 +msgid "Inherit" +msgstr "继承" + +#: perms/templates/perms/asset_permission_list.html:192 +#: users/templates/users/user_asset_permission.html:161 +msgid "Include" +msgstr "包含" + +#: perms/templates/perms/asset_permission_list.html:193 +#: users/templates/users/user_asset_permission.html:162 +msgid "Exclude" +msgstr "不包含" + +#: perms/templates/perms/asset_permission_list.html:211 msgid "Refresh success" msgstr "刷新成功" -#: perms/templates/perms/asset_permission_user.html:35 -#: perms/templates/perms/remote_app_permission_user.html:34 +#: perms/templates/perms/asset_permission_user.html:31 +#: perms/templates/perms/database_app_permission_user.html:31 +#: perms/templates/perms/remote_app_permission_user.html:30 msgid "User list of " msgstr "用户列表" -#: perms/templates/perms/asset_permission_user.html:80 +#: perms/templates/perms/asset_permission_user.html:66 msgid "Add user to asset permission" msgstr "添加用户" -#: perms/templates/perms/asset_permission_user.html:108 +#: perms/templates/perms/asset_permission_user.html:91 msgid "Add user group to asset permission" msgstr "添加用户组" -#: perms/templates/perms/asset_permission_user.html:116 -#: perms/templates/perms/remote_app_permission_user.html:115 +#: perms/templates/perms/asset_permission_user.html:99 +#: perms/templates/perms/database_app_permission_user.html:99 +#: perms/templates/perms/remote_app_permission_user.html:111 msgid "Select user groups" msgstr "选择用户组" -#: perms/templates/perms/remote_app_permission_detail.html:74 +#: perms/templates/perms/database_app_permission_database_app.html:31 +msgid "DatabaseApp list of " +msgstr "数据库应用列表" + +#: perms/templates/perms/database_app_permission_database_app.html:66 +msgid "Add DatabaseApp to this permission" +msgstr "添加数据库应用" + +#: perms/templates/perms/database_app_permission_database_app.html:74 +msgid "Select DatabaseApp" +msgstr "选择数据库应用" + +#: perms/templates/perms/database_app_permission_detail.html:69 +msgid "DatabaseApp count" +msgstr "数据库应用数量" + +#: perms/templates/perms/database_app_permission_user.html:66 +msgid "Add user to permission" +msgstr "添加用户" + +#: perms/templates/perms/database_app_permission_user.html:91 +msgid "Add user group to permission" +msgstr "添加用户组" + +#: perms/templates/perms/remote_app_permission_detail.html:69 msgid "RemoteApp count" msgstr "远程应用数量" -#: perms/templates/perms/remote_app_permission_remote_app.html:34 +#: perms/templates/perms/remote_app_permission_remote_app.html:30 msgid "RemoteApp list of " msgstr "远程应用列表" -#: perms/templates/perms/remote_app_permission_remote_app.html:79 +#: perms/templates/perms/remote_app_permission_remote_app.html:75 msgid "Add RemoteApp to this permission" msgstr "添加远程应用" -#: perms/templates/perms/remote_app_permission_remote_app.html:87 +#: perms/templates/perms/remote_app_permission_remote_app.html:83 msgid "Select RemoteApp" msgstr "选择远程应用" -#: perms/templates/perms/remote_app_permission_user.html:79 +#: perms/templates/perms/remote_app_permission_user.html:75 msgid "Add user to this permission" msgstr "添加用户" -#: perms/templates/perms/remote_app_permission_user.html:107 +#: perms/templates/perms/remote_app_permission_user.html:103 msgid "Add user group to this permission" msgstr "添加用户组" #: perms/views/asset_permission.py:33 perms/views/asset_permission.py:65 #: perms/views/asset_permission.py:82 perms/views/asset_permission.py:99 -#: perms/views/asset_permission.py:139 perms/views/asset_permission.py:170 +#: perms/views/asset_permission.py:136 perms/views/asset_permission.py:169 +#: perms/views/database_app_permission.py:33 +#: perms/views/database_app_permission.py:48 +#: perms/views/database_app_permission.py:64 +#: perms/views/database_app_permission.py:79 +#: perms/views/database_app_permission.py:108 +#: perms/views/database_app_permission.py:143 #: perms/views/remote_app_permission.py:33 #: perms/views/remote_app_permission.py:49 #: perms/views/remote_app_permission.py:66 -#: perms/views/remote_app_permission.py:81 -#: perms/views/remote_app_permission.py:115 -#: perms/views/remote_app_permission.py:148 templates/_nav.html:69 +#: perms/views/remote_app_permission.py:84 +#: perms/views/remote_app_permission.py:116 +#: perms/views/remote_app_permission.py:149 templates/_nav.html:75 #: xpack/plugins/orgs/templates/orgs/org_list.html:22 msgid "Perms" msgstr "权限管理" @@ -3361,7 +3670,7 @@ msgstr "更新资产授权" msgid "Asset permission detail" msgstr "资产授权详情" -#: perms/views/asset_permission.py:140 +#: perms/views/asset_permission.py:137 msgid "Asset permission user list" msgstr "资产授权用户列表" @@ -3369,6 +3678,30 @@ msgstr "资产授权用户列表" msgid "Asset permission asset list" msgstr "资产授权资产列表" +#: perms/views/database_app_permission.py:34 +msgid "DatabaseApp permission list" +msgstr "数据库应用授权列表" + +#: perms/views/database_app_permission.py:49 +msgid "Create DatabaseApp permission" +msgstr "创建数据库应用授权规则" + +#: perms/views/database_app_permission.py:65 +msgid "Update DatabaseApp permission" +msgstr "更新数据库应用授权规则" + +#: perms/views/database_app_permission.py:80 +msgid "DatabaseApp permission detail" +msgstr "数据库应用授权详情" + +#: perms/views/database_app_permission.py:109 +msgid "DatabaseApp permission user list" +msgstr "数据库应用授权用户列表" + +#: perms/views/database_app_permission.py:149 +msgid "DatabaseApp permission DatabaseApp list" +msgstr "数据库应用授权数据库应用列表" + #: perms/views/remote_app_permission.py:34 msgid "RemoteApp permission list" msgstr "远程应用授权列表" @@ -3381,15 +3714,15 @@ msgstr "创建远程应用授权规则" msgid "Update RemoteApp permission" msgstr "更新远程应用授权规则" -#: perms/views/remote_app_permission.py:82 +#: perms/views/remote_app_permission.py:85 msgid "RemoteApp permission detail" msgstr "远程应用授权详情" -#: perms/views/remote_app_permission.py:116 +#: perms/views/remote_app_permission.py:117 msgid "RemoteApp permission user list" msgstr "远程应用授权用户列表" -#: perms/views/remote_app_permission.py:149 +#: perms/views/remote_app_permission.py:150 msgid "RemoteApp permission RemoteApp list" msgstr "远程应用授权远程应用列表" @@ -3403,137 +3736,156 @@ msgstr "连接LDAP成功" #: settings/api.py:107 msgid "LDAP attr map not valid" -msgstr "LDAP 属性映射无效" +msgstr "" #: settings/api.py:116 msgid "Match {} s users" msgstr "匹配 {} 个用户" -#: settings/api.py:224 +#: settings/api.py:226 msgid "Get ldap users is None" msgstr "获取 LDAP 用户为 None" -#: settings/api.py:231 +#: settings/api.py:233 msgid "Imported {} users successfully" msgstr "导入 {} 个用户成功" -#: settings/api.py:262 settings/api.py:298 -msgid "" -"Error: Account invalid (Please make sure the information such as Access key " -"or Secret key is correct)" -msgstr "错误:账户无效 (请确保 Access key 或 Secret key 等信息正确)" - -#: settings/api.py:268 settings/api.py:304 -msgid "Create succeed" -msgstr "创建成功" - -#: settings/api.py:286 settings/api.py:324 -#: settings/templates/settings/terminal_setting.html:154 -msgid "Delete succeed" -msgstr "删除成功" - -#: settings/forms.py:60 +#: settings/forms/basic.py:13 msgid "Current SITE URL" msgstr "当前站点URL" -#: settings/forms.py:64 +#: settings/forms/basic.py:17 msgid "User Guide URL" msgstr "用户向导URL" -#: settings/forms.py:65 +#: settings/forms/basic.py:18 msgid "User first login update profile done redirect to it" msgstr "用户第一次登录,修改profile后重定向到地址" -#: settings/forms.py:68 +#: settings/forms/basic.py:21 msgid "Email Subject Prefix" msgstr "Email主题前缀" -#: settings/forms.py:69 +#: settings/forms/basic.py:22 msgid "Tips: Some word will be intercept by mail provider" msgstr "提示: 一些关键字可能会被邮件提供商拦截,如 跳板机、Jumpserver" -#: settings/forms.py:75 +#: settings/forms/email.py:15 msgid "SMTP host" msgstr "SMTP主机" -#: settings/forms.py:77 +#: settings/forms/email.py:17 msgid "SMTP port" msgstr "SMTP端口" -#: settings/forms.py:79 +#: settings/forms/email.py:19 msgid "SMTP user" msgstr "SMTP账号" -#: settings/forms.py:82 +#: settings/forms/email.py:22 msgid "SMTP password" msgstr "SMTP密码" -#: settings/forms.py:84 +#: settings/forms/email.py:24 msgid "Tips: Some provider use token except password" msgstr "提示:一些邮件提供商需要输入的是Token" -#: settings/forms.py:87 +#: settings/forms/email.py:27 msgid "Send user" msgstr "发送账号" -#: settings/forms.py:89 +#: settings/forms/email.py:29 msgid "Tips: Send mail account, default SMTP account as the send account" msgstr "提示:发送邮件账号,默认使用SMTP账号作为发送账号" -#: settings/forms.py:93 +#: settings/forms/email.py:33 msgid "Test recipient" msgstr "测试收件人" -#: settings/forms.py:94 +#: settings/forms/email.py:34 msgid "Tips: Used only as a test mail recipient" msgstr "提示:仅用来作为测试邮件收件人" -#: settings/forms.py:97 +#: settings/forms/email.py:37 msgid "Use SSL" msgstr "使用SSL" -#: settings/forms.py:98 +#: settings/forms/email.py:38 msgid "If SMTP port is 465, may be select" msgstr "如果SMTP端口是465,通常需要启用SSL" -#: settings/forms.py:101 +#: settings/forms/email.py:41 msgid "Use TLS" msgstr "使用TLS" -#: settings/forms.py:102 +#: settings/forms/email.py:42 msgid "If SMTP port is 587, may be select" msgstr "如果SMTP端口是587,通常需要启用TLS" -#: settings/forms.py:108 +#: settings/forms/email.py:48 +msgid "Create user email subject" +msgstr "创建用户邮件的主题" + +#: settings/forms/email.py:49 +msgid "" +"Tips: When creating a user, send the subject of the email (eg:Create account " +"successfully)" +msgstr "提示: 创建用户时,发送设置密码邮件的主题 (例如: 创建用户成功)" + +#: settings/forms/email.py:53 +msgid "Create user honorific" +msgstr "创建用户邮件的敬语" + +#: settings/forms/email.py:54 +msgid "Tips: When creating a user, send the honorific of the email (eg:Hello)" +msgstr "提示: 创建用户时,发送设置密码邮件的敬语 (例如: 您好)" + +#: settings/forms/email.py:59 +msgid "Create user email content" +msgstr "创建用户邮件的内容" + +#: settings/forms/email.py:60 +msgid "Tips:When creating a user, send the content of the email" +msgstr "提示: 创建用户时,发送设置密码邮件的内容" + +#: settings/forms/email.py:63 +msgid "Signature" +msgstr "署名" + +#: settings/forms/email.py:64 +msgid "Tips: Email signature (eg:jumpserver)" +msgstr "提示: 邮件的署名 (例如: jumpserver)" + +#: settings/forms/ldap.py:16 msgid "LDAP server" msgstr "LDAP地址" -#: settings/forms.py:111 +#: settings/forms/ldap.py:19 msgid "Bind DN" msgstr "绑定DN" -#: settings/forms.py:118 +#: settings/forms/ldap.py:26 msgid "User OU" msgstr "用户OU" -#: settings/forms.py:119 +#: settings/forms/ldap.py:27 msgid "Use | split User OUs" msgstr "使用|分隔各OU" -#: settings/forms.py:123 +#: settings/forms/ldap.py:31 msgid "User search filter" msgstr "用户过滤器" -#: settings/forms.py:124 +#: settings/forms/ldap.py:32 #, python-format msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)" msgstr "可能的选项是(cn或uid或sAMAccountName=%(user)s)" -#: settings/forms.py:127 +#: settings/forms/ldap.py:35 msgid "User attr map" msgstr "LDAP属性映射" -#: settings/forms.py:129 +#: settings/forms/ldap.py:37 msgid "" "User attr map present how to map LDAP user attr to jumpserver, username,name," "email is jumpserver attr" @@ -3541,112 +3893,66 @@ msgstr "" "用户属性映射代表怎样将LDAP中用户属性映射到jumpserver用户上,username, name," "email 是jumpserver的属性" -#: settings/forms.py:138 +#: settings/forms/ldap.py:46 msgid "Enable LDAP auth" msgstr "启用LDAP认证" -#: settings/forms.py:148 -msgid "Auto" -msgstr "自动" - -#: settings/forms.py:155 -msgid "Password auth" -msgstr "密码认证" +#: settings/forms/security.py:16 +msgid "MFA Secondary certification" +msgstr "MFA 二次认证" -#: settings/forms.py:158 -msgid "Public key auth" -msgstr "密钥认证" +#: settings/forms/security.py:18 +msgid "" +"After opening, the user login must use MFA secondary authentication (valid " +"for all users, including administrators)" +msgstr "开启后,用户登录必须使用MFA二次认证(对所有用户有效,包括管理员)" -#: settings/forms.py:161 -msgid "Heartbeat interval" -msgstr "心跳间隔" +#: settings/forms/security.py:24 +msgid "Batch execute commands" +msgstr "批量命令" -#: settings/forms.py:165 -msgid "List sort by" -msgstr "资产列表排序" - -#: settings/forms.py:168 -msgid "List page size" -msgstr "资产分页每页数量" - -#: settings/forms.py:171 -msgid "Session keep duration" -msgstr "会话保留时长" - -#: settings/forms.py:172 -msgid "" -"Units: days, Session, record, command will be delete if more than duration, " -"only in database" -msgstr "" -"单位:天。 会话、录像、命令记录超过该时长将会被删除(仅影响数据库存储, oss等不" -"受影响)" - -#: settings/forms.py:176 -msgid "Telnet login regex" -msgstr "Telnet 成功正则表达式" - -#: settings/forms.py:177 -msgid "ex: Last\\s*login|success|成功" -msgstr "" -"登录telnet服务器成功后的提示正则表达式,如: Last\\s*login|success|成功 " - -#: settings/forms.py:188 -msgid "MFA Secondary certification" -msgstr "MFA 二次认证" - -#: settings/forms.py:190 -msgid "" -"After opening, the user login must use MFA secondary authentication (valid " -"for all users, including administrators)" -msgstr "开启后,用户登录必须使用MFA二次认证(对所有用户有效,包括管理员)" - -#: settings/forms.py:196 -msgid "Batch execute commands" -msgstr "批量命令" - -#: settings/forms.py:197 +#: settings/forms/security.py:25 msgid "Allow user batch execute commands" msgstr "允许用户批量执行命令" -#: settings/forms.py:200 +#: settings/forms/security.py:28 msgid "Service account registration" msgstr "终端注册" -#: settings/forms.py:201 +#: settings/forms/security.py:29 msgid "" "Allow using bootstrap token register service account, when terminal setup, " "can disable it" msgstr "允许使用bootstrap token注册终端, 当终端注册成功后可以禁止" -#: settings/forms.py:207 +#: settings/forms/security.py:35 msgid "Limit the number of login failures" msgstr "限制登录失败次数" -#: settings/forms.py:211 +#: settings/forms/security.py:39 msgid "No logon interval" msgstr "禁止登录时间间隔" -#: settings/forms.py:213 +#: settings/forms/security.py:41 msgid "" "Tip: (unit/minute) if the user has failed to log in for a limited number of " "times, no login is allowed during this time interval." msgstr "" "提示:(单位:分)当用户登录失败次数达到限制后,那么在此时间间隔内禁止登录" -#: settings/forms.py:220 +#: settings/forms/security.py:48 msgid "Connection max idle time" -msgstr "SSH最大空闲时间" +msgstr "连接最大空闲时间" -#: settings/forms.py:222 -msgid "" -"If idle time more than it, disconnect connection(only ssh now) Unit: minute" -msgstr "提示:(单位:分)如果超过该配置没有操作,连接会被断开(仅ssh)" +#: settings/forms/security.py:50 +msgid "If idle time more than it, disconnect connection Unit: minute" +msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)" -#: settings/forms.py:228 +#: settings/forms/security.py:56 msgid "Password expiration time" msgstr "密码过期时间" -#: settings/forms.py:230 +#: settings/forms/security.py:58 msgid "" "Tip: (unit: day) If the user does not update the password during the time, " "the user password will expire failure;The password expiration reminder mail " @@ -3656,85 +3962,96 @@ msgstr "" "提示:(单位:天)如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期" "提醒邮件将在密码过期前5天内由系统(每天)自动发送给用户" -#: settings/forms.py:239 +#: settings/forms/security.py:67 msgid "Password minimum length" msgstr "密码最小长度 " -#: settings/forms.py:243 +#: settings/forms/security.py:71 msgid "Must contain capital letters" msgstr "必须包含大写字母" -#: settings/forms.py:245 +#: settings/forms/security.py:73 msgid "" "After opening, the user password changes and resets must contain uppercase " "letters" msgstr "开启后,用户密码修改、重置必须包含大写字母" -#: settings/forms.py:250 +#: settings/forms/security.py:78 msgid "Must contain lowercase letters" msgstr "必须包含小写字母" -#: settings/forms.py:251 +#: settings/forms/security.py:79 msgid "" "After opening, the user password changes and resets must contain lowercase " "letters" msgstr "开启后,用户密码修改、重置必须包含小写字母" -#: settings/forms.py:256 +#: settings/forms/security.py:84 msgid "Must contain numeric characters" msgstr "必须包含数字字符" -#: settings/forms.py:257 +#: settings/forms/security.py:85 msgid "" "After opening, the user password changes and resets must contain numeric " "characters" msgstr "开启后,用户密码修改、重置必须包含数字字符" -#: settings/forms.py:262 +#: settings/forms/security.py:90 msgid "Must contain special characters" msgstr "必须包含特殊字符" -#: settings/forms.py:263 +#: settings/forms/security.py:91 msgid "" "After opening, the user password changes and resets must contain special " "characters" msgstr "开启后,用户密码修改、重置必须包含特殊字符" -#: settings/forms.py:270 -msgid "Create user email subject" -msgstr "创建用户邮件的主题" +#: settings/forms/terminal.py:20 +msgid "Auto" +msgstr "自动" -#: settings/forms.py:271 -msgid "" -"Tips: When creating a user, send the subject of the email (eg:Create account " -"successfully)" -msgstr "提示: 创建用户时,发送设置密码邮件的主题 (例如: 创建用户成功)" +#: settings/forms/terminal.py:27 +msgid "Password auth" +msgstr "密码认证" -#: settings/forms.py:275 -msgid "Create user honorific" -msgstr "创建用户邮件的敬语" +#: settings/forms/terminal.py:30 +msgid "Public key auth" +msgstr "密钥认证" -#: settings/forms.py:276 -msgid "Tips: When creating a user, send the honorific of the email (eg:Hello)" -msgstr "提示: 创建用户时,发送设置密码邮件的敬语 (例如: 您好)" +#: settings/forms/terminal.py:33 +msgid "Heartbeat interval" +msgstr "心跳间隔" -#: settings/forms.py:281 -msgid "Create user email content" -msgstr "创建用户邮件的内容" +#: settings/forms/terminal.py:37 +msgid "List sort by" +msgstr "资产列表排序" -#: settings/forms.py:282 -msgid "Tips:When creating a user, send the content of the email" -msgstr "提示: 创建用户时,发送设置密码邮件的内容" +#: settings/forms/terminal.py:40 +msgid "List page size" +msgstr "资产分页每页数量" -#: settings/forms.py:285 -msgid "Signature" -msgstr "署名" +#: settings/forms/terminal.py:43 +msgid "Session keep duration" +msgstr "会话保留时长" -#: settings/forms.py:286 -msgid "Tips: Email signature (eg:jumpserver)" -msgstr "提示: 邮件的署名 (例如: jumpserver)" +#: settings/forms/terminal.py:44 +msgid "" +"Units: days, Session, record, command will be delete if more than duration, " +"only in database" +msgstr "" +"单位:天。 会话、录像、命令记录超过该时长将会被删除(仅影响数据库存储, oss等不" +"受影响)" + +#: settings/forms/terminal.py:48 +msgid "Telnet login regex" +msgstr "Telnet 成功正则表达式" -#: settings/models.py:128 users/templates/users/reset_password.html:68 +#: settings/forms/terminal.py:49 +msgid "ex: Last\\s*login|success|成功" +msgstr "" +"登录telnet服务器成功后的提示正则表达式,如: Last\\s*login|success|成功 " + +#: settings/models.py:95 users/templates/users/reset_password.html:29 #: users/templates/users/user_profile.html:20 msgid "Setting" msgstr "设置" @@ -3752,7 +4069,8 @@ msgid "Refresh cache" msgstr "刷新缓存" #: settings/templates/settings/_ldap_list_users_modal.html:33 -#: users/models/user.py:375 users/templates/users/user_detail.html:71 +#: users/forms/profile.py:89 users/models/user.py:440 +#: users/templates/users/user_detail.html:57 #: users/templates/users/user_profile.html:59 msgid "Email" msgstr "邮件" @@ -3766,214 +4084,102 @@ msgid "" "User is not currently selected, please check the user you want to import" msgstr "当前无勾选用户,请勾选你想要导入的用户" -#: settings/templates/settings/basic_setting.html:15 -#: settings/templates/settings/email_content_setting.html:15 -#: settings/templates/settings/email_setting.html:15 -#: settings/templates/settings/ldap_setting.html:15 -#: settings/templates/settings/security_setting.html:15 -#: settings/templates/settings/terminal_setting.html:16 -#: settings/templates/settings/terminal_setting.html:49 settings/views.py:21 +#: settings/templates/settings/_ldap_list_users_modal.html:172 +#: templates/_csv_import_export.html:13 templates/_csv_import_modal.html:5 +#: xpack/plugins/license/templates/license/license_detail.html:88 +msgid "Import" +msgstr "导入" + +#: settings/templates/settings/_setting_tabs.html:4 +#: settings/templates/settings/terminal_setting.html:31 settings/views.py:20 msgid "Basic setting" msgstr "基本设置" -#: settings/templates/settings/basic_setting.html:18 -#: settings/templates/settings/email_content_setting.html:18 -#: settings/templates/settings/email_setting.html:18 -#: settings/templates/settings/ldap_setting.html:18 -#: settings/templates/settings/security_setting.html:18 -#: settings/templates/settings/terminal_setting.html:20 settings/views.py:48 +#: settings/templates/settings/_setting_tabs.html:7 settings/views.py:47 msgid "Email setting" msgstr "邮件设置" -#: settings/templates/settings/basic_setting.html:21 -#: settings/templates/settings/email_content_setting.html:21 -#: settings/templates/settings/email_setting.html:21 -#: settings/templates/settings/ldap_setting.html:21 -#: settings/templates/settings/security_setting.html:21 -#: settings/templates/settings/terminal_setting.html:23 settings/views.py:188 +#: settings/templates/settings/_setting_tabs.html:10 settings/views.py:162 msgid "Email content setting" msgstr "邮件内容设置" -#: settings/templates/settings/basic_setting.html:24 -#: settings/templates/settings/email_content_setting.html:24 -#: settings/templates/settings/email_setting.html:24 -#: settings/templates/settings/ldap_setting.html:24 -#: settings/templates/settings/security_setting.html:24 -#: settings/templates/settings/terminal_setting.html:27 settings/views.py:75 +#: settings/templates/settings/_setting_tabs.html:13 settings/views.py:74 msgid "LDAP setting" msgstr "LDAP设置" -#: settings/templates/settings/basic_setting.html:27 -#: settings/templates/settings/email_content_setting.html:27 -#: settings/templates/settings/email_setting.html:27 -#: settings/templates/settings/ldap_setting.html:27 -#: settings/templates/settings/security_setting.html:27 -#: settings/templates/settings/terminal_setting.html:31 settings/views.py:106 +#: settings/templates/settings/_setting_tabs.html:16 settings/views.py:106 msgid "Terminal setting" msgstr "终端设置" -#: settings/templates/settings/basic_setting.html:30 -#: settings/templates/settings/email_content_setting.html:30 -#: settings/templates/settings/email_setting.html:30 -#: settings/templates/settings/ldap_setting.html:30 -#: settings/templates/settings/security_setting.html:30 -#: settings/templates/settings/security_setting.html:45 -#: settings/templates/settings/terminal_setting.html:34 settings/views.py:161 +#: settings/templates/settings/_setting_tabs.html:19 +#: settings/templates/settings/security_setting.html:26 settings/views.py:135 msgid "Security setting" msgstr "安全设置" -#: settings/templates/settings/command_storage_create.html:52 -msgid "Tips: If there are multiple hosts, separate them with a comma (,)" -msgstr "提示: 如果有多台主机,请使用逗号 ( , ) 进行分割" - -#: settings/templates/settings/command_storage_create.html:63 -msgid "Index" -msgstr "索引" - -#: settings/templates/settings/command_storage_create.html:70 -msgid "Doc type" -msgstr "文档类型" - -#: settings/templates/settings/email_content_setting.html:45 +#: settings/templates/settings/email_content_setting.html:26 msgid "Create User setting" msgstr "创建用户设置" -#: settings/templates/settings/ldap_setting.html:66 +#: settings/templates/settings/ldap_setting.html:47 msgid "Bulk import" msgstr "一键导入" -#: settings/templates/settings/replay_storage_create.html:66 -msgid "Bucket" -msgstr "桶名称" - -#: settings/templates/settings/replay_storage_create.html:73 -msgid "Access key" -msgstr "" - -#: settings/templates/settings/replay_storage_create.html:80 -msgid "Secret key" -msgstr "" - -#: settings/templates/settings/replay_storage_create.html:87 -msgid "Container name" -msgstr "容器名称" - -#: settings/templates/settings/replay_storage_create.html:94 -msgid "Account name" -msgstr "账户名称" - -#: settings/templates/settings/replay_storage_create.html:101 -msgid "Account key" -msgstr "账户密钥" - -#: settings/templates/settings/replay_storage_create.html:108 -msgid "Endpoint" -msgstr "端点" - -#: settings/templates/settings/replay_storage_create.html:114 -#, python-brace-format -msgid "OSS: http://{REGION_NAME}.aliyuncs.com" -msgstr "OSS: http://{REGION_NAME}.aliyuncs.com" - -#: settings/templates/settings/replay_storage_create.html:116 -msgid "Example: http://oss-cn-hangzhou.aliyuncs.com" -msgstr "如: http://oss-cn-hangzhou.aliyuncs.com" - -#: settings/templates/settings/replay_storage_create.html:118 -#, python-brace-format -msgid "S3: http://s3.{REGION_NAME}.amazonaws.com" -msgstr "S3: http://s3.{REGION_NAME}.amazonaws.com" - -#: settings/templates/settings/replay_storage_create.html:119 -#, python-brace-format -msgid "S3(China): http://s3.{REGION_NAME}.amazonaws.com.cn" -msgstr "S3(中国): http://s3.{REGION_NAME}.amazonaws.com.cn" - -#: settings/templates/settings/replay_storage_create.html:120 -msgid "Example: http://s3.cn-north-1.amazonaws.com.cn" -msgstr "如: http://s3.cn-north-1.amazonaws.com.cn" - -#: settings/templates/settings/replay_storage_create.html:126 -msgid "Endpoint suffix" -msgstr "端点后缀" - -#: settings/templates/settings/replay_storage_create.html:136 -#: xpack/plugins/cloud/models.py:304 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:109 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:62 -msgid "Region" -msgstr "地域" - -#: settings/templates/settings/replay_storage_create.html:141 -msgid "Beijing: cn-north-1" -msgstr "北京: cn-north-1" - -#: settings/templates/settings/replay_storage_create.html:142 -msgid "Ningxia: cn-northwest-1" -msgstr "宁夏: cn-northwest-1" - -#: settings/templates/settings/replay_storage_create.html:143 -msgid "More" -msgstr "更多" - -#: settings/templates/settings/replay_storage_create.html:247 -msgid "Submitting" -msgstr "提交中" - -#: settings/templates/settings/replay_storage_create.html:256 -msgid "Endpoint need contain protocol, ex: http" -msgstr "端点需要包含协议,如 http" - -#: settings/templates/settings/security_setting.html:49 +#: settings/templates/settings/security_setting.html:30 msgid "Password check rule" msgstr "密码校验规则" -#: settings/templates/settings/terminal_setting.html:79 terminal/forms.py:27 -#: terminal/models.py:27 -msgid "Command storage" -msgstr "命令存储" +#: settings/templates/settings/terminal_setting.html:7 +msgid "Command and Replay storage configuration migrated to" +msgstr "命令和录像存储配置已迁移到" -#: settings/templates/settings/terminal_setting.html:101 terminal/forms.py:32 -#: terminal/models.py:28 -msgid "Replay storage" -msgstr "录像存储" +#: settings/templates/settings/terminal_setting.html:8 +msgid "Sessions -> Terminal -> Storage configuration" +msgstr "会话管理 -> 终端管理 -> 存储配置" -#: settings/templates/settings/terminal_setting.html:157 -msgid "Delete failed" -msgstr "删除失败" +#: settings/templates/settings/terminal_setting.html:9 +msgid "Here" +msgstr "这里" -#: settings/templates/settings/terminal_setting.html:162 -msgid "Are you sure about deleting it?" -msgstr "您确定删除吗?" - -#: settings/utils/ldap.py:128 -msgid "Search no entry matched in ou {}" -msgstr "在ou:{}中没有匹配条目" - -#: settings/views.py:20 settings/views.py:47 settings/views.py:74 -#: settings/views.py:105 settings/views.py:133 settings/views.py:146 -#: settings/views.py:160 settings/views.py:187 templates/_nav.html:170 +#: settings/views.py:19 settings/views.py:46 settings/views.py:73 +#: settings/views.py:105 settings/views.py:134 settings/views.py:161 +#: templates/_nav.html:187 msgid "Settings" msgstr "系统设置" -#: settings/views.py:31 settings/views.py:58 settings/views.py:85 -#: settings/views.py:118 settings/views.py:171 settings/views.py:198 +#: settings/views.py:30 settings/views.py:57 settings/views.py:84 +#: settings/views.py:118 settings/views.py:145 settings/views.py:172 msgid "Update setting successfully" msgstr "更新设置成功" -#: settings/views.py:134 -msgid "Create replay storage" -msgstr "创建录像存储" +#: templates/_csv_import_modal.html:12 +msgid "Download the imported template or use the exported CSV file format" +msgstr "下载导入的模板或使用导出的csv格式" -#: settings/views.py:147 -msgid "Create command storage" -msgstr "创建命令存储" +#: templates/_csv_import_modal.html:13 +msgid "Download the import template" +msgstr "下载导入模版" + +#: templates/_csv_import_modal.html:17 templates/_csv_update_modal.html:17 +msgid "Select the CSV file to import" +msgstr "请选择csv文件导入" + +#: templates/_csv_import_modal.html:39 templates/_csv_update_modal.html:42 +msgid "Please select file" +msgstr "选择文件" + +#: templates/_csv_update_modal.html:12 +msgid "Download the update template or use the exported CSV file format" +msgstr "下载更新的模板或使用导出的csv格式" + +#: templates/_csv_update_modal.html:13 +msgid "Download the update template" +msgstr "下载更新模版" #: templates/_header_bar.html:12 msgid "Help" msgstr "帮助" -#: templates/_header_bar.html:19 users/templates/users/_base_otp.html:29 +#: templates/_header_bar.html:19 templates/_without_nav_base.html:28 msgid "Docs" msgstr "文档" @@ -3982,14 +4188,14 @@ msgid "Commercial support" msgstr "商业支持" #: templates/_header_bar.html:70 templates/_nav.html:30 -#: templates/_nav_user.html:32 users/forms.py:173 +#: templates/_nav_user.html:37 users/forms/profile.py:31 #: users/templates/users/_user.html:44 #: users/templates/users/first_login.html:39 #: users/templates/users/user_password_update.html:40 #: users/templates/users/user_profile.html:17 #: users/templates/users/user_profile_update.html:37 #: users/templates/users/user_profile_update.html:61 -#: users/templates/users/user_pubkey_update.html:37 users/views/user.py:231 +#: users/templates/users/user_pubkey_update.html:37 users/views/profile.py:51 msgid "Profile" msgstr "个人信息" @@ -4009,18 +4215,6 @@ msgstr "" msgid "Logout" msgstr "注销登录" -#: templates/_import_modal.html:12 -msgid "Download the imported template or use the exported CSV file format" -msgstr "下载导入的模板或使用导出的csv格式" - -#: templates/_import_modal.html:13 -msgid "Download the import template" -msgstr "下载导入模版" - -#: templates/_import_modal.html:17 templates/_update_modal.html:17 -msgid "Select the CSV file to import" -msgstr "请选择csv文件导入" - #: templates/_message.html:6 msgid "" "\n" @@ -4096,13 +4290,15 @@ msgstr "" #: templates/_nav.html:17 users/views/group.py:28 users/views/group.py:45 #: users/views/group.py:63 users/views/group.py:82 users/views/group.py:99 -#: users/views/login.py:154 users/views/user.py:61 users/views/user.py:78 -#: users/views/user.py:122 users/views/user.py:189 users/views/user.py:217 -#: users/views/user.py:270 users/views/user.py:305 +#: users/views/login.py:158 users/views/profile.py:90 +#: users/views/profile.py:125 users/views/user.py:50 users/views/user.py:67 +#: users/views/user.py:111 users/views/user.py:178 users/views/user.py:206 +#: users/views/user.py:220 users/views/user.py:234 users/views/user.py:248 +#: users/views/user.py:262 users/views/user.py:276 msgid "Users" msgstr "用户管理" -#: templates/_nav.html:20 users/views/user.py:62 +#: templates/_nav.html:20 users/views/user.py:51 msgid "User list" msgstr "用户列表" @@ -4110,62 +4306,69 @@ msgstr "用户列表" msgid "Command filters" msgstr "命令过滤" -#: templates/_nav.html:88 terminal/views/command.py:21 +#: templates/_nav.html:97 terminal/views/command.py:21 #: terminal/views/session.py:43 terminal/views/session.py:54 #: terminal/views/session.py:78 terminal/views/terminal.py:32 #: terminal/views/terminal.py:48 terminal/views/terminal.py:61 msgid "Sessions" msgstr "会话管理" -#: templates/_nav.html:91 +#: templates/_nav.html:100 msgid "Session online" msgstr "在线会话" -#: templates/_nav.html:92 terminal/views/session.py:55 +#: templates/_nav.html:101 terminal/views/session.py:55 msgid "Session offline" msgstr "历史会话" -#: templates/_nav.html:93 +#: templates/_nav.html:102 msgid "Commands" msgstr "命令记录" -#: templates/_nav.html:96 templates/_nav_user.html:37 +#: templates/_nav.html:105 templates/_nav_user.html:42 msgid "Web terminal" msgstr "Web终端" -#: templates/_nav.html:97 templates/_nav_user.html:42 +#: templates/_nav.html:106 templates/_nav_user.html:47 msgid "File manager" msgstr "文件管理" -#: templates/_nav.html:101 +#: templates/_nav.html:110 terminal/views/storage.py:27 +#: terminal/views/storage.py:42 terminal/views/storage.py:96 +#: terminal/views/storage.py:120 terminal/views/storage.py:149 +#: terminal/views/storage.py:175 msgid "Terminal" msgstr "终端管理" -#: templates/_nav.html:112 +#: templates/_nav.html:121 msgid "Job Center" msgstr "作业中心" -#: templates/_nav.html:116 templates/_nav.html:136 +#: templates/_nav.html:125 templates/_nav.html:153 msgid "Batch command" msgstr "批量命令" -#: templates/_nav.html:118 +#: templates/_nav.html:127 msgid "Task monitor" msgstr "任务监控" -#: templates/_nav.html:146 +#: templates/_nav.html:137 tickets/views.py:19 tickets/views.py:37 +msgid "Tickets" +msgstr "工单管理" + +#: templates/_nav.html:163 msgid "XPack" msgstr "" -#: templates/_nav.html:154 xpack/plugins/cloud/views.py:28 +#: templates/_nav.html:171 xpack/plugins/cloud/views.py:28 msgid "Account list" msgstr "账户列表" -#: templates/_nav.html:155 +#: templates/_nav.html:172 msgid "Sync instance" msgstr "同步实例" -#: templates/_nav_user.html:11 +#: templates/_nav_user.html:10 msgid "My Applications" msgstr "我的应用" @@ -4174,26 +4377,18 @@ msgid "" "Displays the results of items _START_ to _END_; A total of _TOTAL_ entries" msgstr "显示第 _START_ 至 _END_ 项结果; 总共 _TOTAL_ 项" -#: templates/_update_modal.html:12 -msgid "Download the update template or use the exported CSV file format" -msgstr "下载更新的模板或使用导出的csv格式" - -#: templates/_update_modal.html:13 -msgid "Download the update template" -msgstr "下载更新模版" +#: templates/_without_nav_base.html:26 +msgid "Home page" +msgstr "首页" #: templates/captcha/image.html:3 msgid "Play CAPTCHA as audio file" msgstr "语言播放验证码" -#: templates/captcha/text_field.html:4 +#: templates/captcha/text_field.html:4 users/forms/profile.py:90 msgid "Captcha" msgstr "验证码" -#: templates/flash_message_standalone.html:47 -msgid "Return" -msgstr "返回" - #: templates/index.html:11 msgid "Total users" msgstr "用户总数" @@ -4348,22 +4543,132 @@ msgstr "月未登录主机" msgid "Filters" msgstr "过滤" +#: terminal/api/storage.py:24 +msgid "Deleting the default storage is not allowed" +msgstr "不允许删除默认存储配置" + +#: terminal/api/storage.py:54 +msgid "Test failure: {}" +msgstr "测试失败: {}" + +#: terminal/api/storage.py:57 +msgid "Test successful" +msgstr "测试成功" + +#: terminal/api/storage.py:59 +msgid "Test failure: Account invalid" +msgstr "测试失败: 账户无效" + #: terminal/backends/command/models.py:15 msgid "Input" msgstr "输入" #: terminal/backends/command/models.py:17 #: terminal/templates/terminal/command_list.html:32 -#: terminal/templates/terminal/terminal_list.html:33 +#: terminal/templates/terminal/terminal_list.html:34 msgid "Session" msgstr "会话" -#: terminal/forms.py:28 +#: terminal/forms/storage.py:41 +msgid "Container name" +msgstr "容器名称" + +#: terminal/forms/storage.py:44 +msgid "Account name" +msgstr "账户名称" + +#: terminal/forms/storage.py:47 +msgid "Account key" +msgstr "账户密钥" + +#: terminal/forms/storage.py:55 +msgid "Endpoint suffix" +msgstr "端点后缀" + +#: terminal/forms/storage.py:61 terminal/forms/storage.py:84 +#: terminal/forms/storage.py:108 terminal/forms/storage.py:132 +msgid "Bucket" +msgstr "桶名称" + +#: terminal/forms/storage.py:64 terminal/forms/storage.py:87 +#: terminal/forms/storage.py:111 terminal/forms/storage.py:135 +msgid "Access key" +msgstr "" + +#: terminal/forms/storage.py:68 terminal/forms/storage.py:91 +#: terminal/forms/storage.py:115 terminal/forms/storage.py:139 +msgid "Secret key" +msgstr "" + +#: terminal/forms/storage.py:72 terminal/forms/storage.py:95 +#: terminal/forms/storage.py:119 terminal/forms/storage.py:146 +msgid "Endpoint" +msgstr "端点" + +#: terminal/forms/storage.py:74 +#, python-brace-format +msgid "" +"\n" +" OSS: http://{REGION_NAME}.aliyuncs.com
    \n" +" Example: http://oss-cn-hangzhou.aliyuncs.com\n" +" " +msgstr "" + +#: terminal/forms/storage.py:97 terminal/forms/storage.py:121 +#, python-brace-format +msgid "" +"\n" +" S3: http://s3.{REGION_NAME}.amazonaws.com
    \n" +" S3(China): http://s3.{REGION_NAME}.amazonaws.com.cn
    \n" +" Example: http://s3.cn-north-1.amazonaws.com.cn\n" +" " +msgstr "" + +#: terminal/forms/storage.py:143 xpack/plugins/cloud/models.py:303 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:106 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:59 +msgid "Region" +msgstr "地域" + +#: terminal/forms/storage.py:160 +msgid "" +"\n" +" Tips: If there are multiple hosts, separate them with a comma " +"(,) \n" +"
    \n" +" eg: http://www.jumpserver.a.com,http://www.jumpserver.b.com\n" +" " +msgstr "" +"\n" +" 提示: 如果有多台主机,请使用逗号 ( , ) 进行分割\n" +"
    \n" +" eg: http://www.jumpserver.a.com,http://www.jumpserver.b.com\n" +" " + +#: terminal/forms/storage.py:168 +msgid "Index" +msgstr "索引" + +#: terminal/forms/storage.py:171 +msgid "Doc type" +msgstr "文档类型" + +#: terminal/forms/terminal.py:25 terminal/models.py:30 +#: terminal/templates/terminal/base_storage_list.html:10 +msgid "Command storage" +msgstr "命令存储" + +#: terminal/forms/terminal.py:26 msgid "Command can store in server db or ES, default to server, more see docs" msgstr "" "命令支持存储到服务器端数据库、ES中,默认存储的服务器端数据库,更多查看文档" -#: terminal/forms.py:33 +#: terminal/forms/terminal.py:30 terminal/models.py:31 +#: terminal/templates/terminal/base_storage_list.html:9 +msgid "Replay storage" +msgstr "录像存储" + +#: terminal/forms/terminal.py:31 msgid "" "Replay file can store in server disk, AWS S3, Aliyun OSS, default to server, " "more see docs" @@ -4371,51 +4676,47 @@ msgstr "" "录像文件支持存储到服务器端硬盘、AWS S3、 阿里云 OSS 中,默认存储到服务器端硬" "盘, 更多查看文档" -#: terminal/models.py:24 +#: terminal/models.py:27 msgid "Remote Address" msgstr "远端地址" -#: terminal/models.py:26 +#: terminal/models.py:29 msgid "HTTP Port" msgstr "HTTP端口" -#: terminal/models.py:126 +#: terminal/models.py:145 msgid "Session Online" msgstr "在线会话" -#: terminal/models.py:127 +#: terminal/models.py:146 msgid "CPU Usage" msgstr "CPU使用" -#: terminal/models.py:128 +#: terminal/models.py:147 msgid "Memory Used" msgstr "内存使用" -#: terminal/models.py:129 +#: terminal/models.py:148 msgid "Connections" msgstr "连接数" -#: terminal/models.py:130 +#: terminal/models.py:149 msgid "Threads" msgstr "线程数" -#: terminal/models.py:131 +#: terminal/models.py:150 msgid "Boot Time" msgstr "运行时间" -#: terminal/models.py:162 terminal/templates/terminal/session_list.html:137 +#: terminal/models.py:185 terminal/templates/terminal/session_list.html:135 msgid "Replay" msgstr "回放" -#: terminal/models.py:166 -msgid "Date last active" -msgstr "最后活跃日期" - -#: terminal/models.py:168 +#: terminal/models.py:190 msgid "Date end" msgstr "结束日期" -#: terminal/models.py:261 +#: terminal/models.py:283 msgid "Args" msgstr "参数" @@ -4427,6 +4728,16 @@ msgstr "导出命令" msgid "Goto" msgstr "转到" +#: terminal/templates/terminal/command_storage_list.html:5 +#: terminal/views/storage.py:150 +msgid "Create command storage" +msgstr "创建命令存储" + +#: terminal/templates/terminal/replay_storage_list.html:5 +#: terminal/views/storage.py:97 +msgid "Create replay storage" +msgstr "创建录像存储" + #: terminal/templates/terminal/session_detail.html:17 #: terminal/views/session.py:79 msgid "Session detail" @@ -4453,31 +4764,35 @@ msgstr "监控" msgid "Terminate session" msgstr "终止会话" -#: terminal/templates/terminal/session_list.html:32 +#: terminal/templates/terminal/session_detail.html:144 +msgid "Terminate success" +msgstr "终断成功" + +#: terminal/templates/terminal/session_list.html:30 msgid "Login from" msgstr "登录来源" -#: terminal/templates/terminal/session_list.html:35 +#: terminal/templates/terminal/session_list.html:33 msgid "Duration" msgstr "时长" -#: terminal/templates/terminal/session_list.html:47 +#: terminal/templates/terminal/session_list.html:45 msgid "Terminate selected" msgstr "终断所选" -#: terminal/templates/terminal/session_list.html:48 +#: terminal/templates/terminal/session_list.html:46 msgid "Confirm finished" msgstr "确认已完成" -#: terminal/templates/terminal/session_list.html:92 +#: terminal/templates/terminal/session_list.html:90 msgid "Terminate task send, waiting ..." msgstr "终断任务已发送,请等待" -#: terminal/templates/terminal/session_list.html:143 +#: terminal/templates/terminal/session_list.html:141 msgid "Terminate" msgstr "终断" -#: terminal/templates/terminal/session_list.html:174 +#: terminal/templates/terminal/session_list.html:172 msgid "Finish session success" msgstr "标记会话完成成功" @@ -4494,19 +4809,25 @@ msgstr "SSH端口" msgid "Http port" msgstr "HTTP端口" -#: terminal/templates/terminal/terminal_list.html:30 +#: terminal/templates/terminal/terminal_list.html:21 +msgid "Storage configuration" +msgstr "存储配置" + +#: terminal/templates/terminal/terminal_list.html:31 msgid "Addr" msgstr "地址" -#: terminal/templates/terminal/terminal_list.html:35 +#: terminal/templates/terminal/terminal_list.html:36 msgid "Alive" msgstr "在线" -#: terminal/templates/terminal/terminal_list.html:78 +#: terminal/templates/terminal/terminal_list.html:79 msgid "Accept" msgstr "接受" -#: terminal/templates/terminal/terminal_list.html:80 +#: terminal/templates/terminal/terminal_list.html:81 +#: tickets/models/ticket.py:31 tickets/templates/tickets/ticket_detail.html:101 +#: tickets/templates/tickets/ticket_list.html:110 msgid "Reject" msgstr "拒绝" @@ -4514,7 +4835,7 @@ msgstr "拒绝" msgid "Accept terminal registration" msgstr "接受终端注册" -#: terminal/templates/terminal/terminal_update.html:33 +#: terminal/templates/terminal/terminal_update.html:31 msgid "Info" msgstr "信息" @@ -4522,6 +4843,22 @@ msgstr "信息" msgid "Session online list" msgstr "在线会话" +#: terminal/views/storage.py:28 +msgid "Replay storage list" +msgstr "录像存储列表" + +#: terminal/views/storage.py:43 +msgid "Command storage list" +msgstr "命令存储列表" + +#: terminal/views/storage.py:121 +msgid "Update replay storage" +msgstr "更新录像存储" + +#: terminal/views/storage.py:176 +msgid "Update command storage" +msgstr "更新命令存储" + #: terminal/views/terminal.py:33 msgid "Terminal list" msgstr "终端列表" @@ -4543,71 +4880,160 @@ msgid "" "You should use your ssh client tools connect terminal: {}

    {}" msgstr "你可以使用ssh客户端工具连接终端" -#: users/api/user.py:173 -msgid "Could not reset self otp, use profile reset instead" -msgstr "不能再该页面重置MFA, 请去个人信息页面重置" +#: tickets/models/ticket.py:18 tickets/models/ticket.py:70 +#: tickets/templates/tickets/ticket_list.html:105 +msgid "Open" +msgstr "开启" -#: users/forms.py:47 users/models/user.py:383 -#: users/templates/users/_select_user_modal.html:15 -#: users/templates/users/user_detail.html:87 -#: users/templates/users/user_list.html:37 -#: users/templates/users/user_profile.html:55 -msgid "Role" -msgstr "角色" +#: tickets/models/ticket.py:19 tickets/templates/tickets/ticket_list.html:106 +msgid "Closed" +msgstr "关闭" -#: users/forms.py:51 users/models/user.py:418 -#: users/templates/users/user_detail.html:103 -#: users/templates/users/user_list.html:39 -#: users/templates/users/user_profile.html:102 -msgid "Source" -msgstr "用户来源" +#: tickets/models/ticket.py:24 +msgid "General" +msgstr "一般" -#: users/forms.py:54 users/forms.py:252 -#: users/templates/users/user_update.html:30 -msgid "ssh public key" -msgstr "ssh公钥" +#: tickets/models/ticket.py:30 tickets/templates/tickets/ticket_detail.html:100 +#: tickets/templates/tickets/ticket_list.html:109 +msgid "Approve" +msgstr "同意" -#: users/forms.py:55 users/forms.py:253 -msgid "ssh-rsa AAAA..." -msgstr "" +#: tickets/models/ticket.py:34 tickets/models/ticket.py:129 +msgid "User display name" +msgstr "用户显示名称" -#: users/forms.py:56 -msgid "Paste user id_rsa.pub here." -msgstr "复制用户公钥到这里" +#: tickets/models/ticket.py:36 tickets/templates/tickets/ticket_list.html:33 +#: tickets/templates/tickets/ticket_list.html:102 +msgid "Title" +msgstr "标题" -#: users/forms.py:71 users/templates/users/user_detail.html:226 -msgid "Join user groups" -msgstr "添加到用户组" +#: tickets/models/ticket.py:37 tickets/models/ticket.py:130 +msgid "Body" +msgstr "内容" -#: users/forms.py:106 users/forms.py:267 -msgid "Public key should not be the same as your old one." -msgstr "不能和原来的密钥相同" +#: tickets/models/ticket.py:39 tickets/templates/tickets/ticket_detail.html:51 +msgid "Assignee" +msgstr "处理人" -#: users/forms.py:110 users/forms.py:271 users/serializers/user.py:110 -msgid "Not a valid ssh public key" -msgstr "ssh密钥不合法" +#: tickets/models/ticket.py:40 +msgid "Assignee display name" +msgstr "处理人名称" -#: users/forms.py:123 users/views/login.py:114 users/views/user.py:287 -msgid "* Your password does not meet the requirements" -msgstr "* 您的密码不符合要求" +#: tickets/models/ticket.py:41 tickets/templates/tickets/ticket_detail.html:50 +msgid "Assignees" +msgstr "待处理人" -#: users/forms.py:144 -msgid "Reset link will be generated and sent to the user" -msgstr "生成重置密码链接,通过邮件发送给用户" +#: tickets/models/ticket.py:42 +msgid "Assignees display name" +msgstr "待处理人名称" -#: users/forms.py:145 -msgid "Set password" -msgstr "设置密码" +#: tickets/models/ticket.py:71 +msgid "{} {} this ticket" +msgstr "{} {} 这个工单" -#: users/forms.py:152 xpack/plugins/change_auth_plan/models.py:88 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:51 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:69 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:57 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:16 -msgid "Password strategy" -msgstr "密码策略" +#: tickets/models/ticket.py:82 +msgid "this ticket" +msgstr "这个工单" + +#: tickets/templates/tickets/ticket_detail.html:66 +#: tickets/templates/tickets/ticket_detail.html:80 +msgid "ago" +msgstr "前" -#: users/forms.py:179 +#: tickets/templates/tickets/ticket_list.html:9 +msgid "My tickets" +msgstr "我的工单" + +#: tickets/templates/tickets/ticket_list.html:10 +msgid "Assigned me" +msgstr "待处理" + +#: tickets/templates/tickets/ticket_list.html:19 +msgid "Create ticket" +msgstr "提交工单" + +#: tickets/utils.py:18 +msgid "New ticket" +msgstr "新工单" + +#: tickets/utils.py:21 +#, python-brace-format +msgid "" +"\n" +"
    \n" +"

    Your has a new ticket

    \n" +"
    \n" +" {body}\n" +"
    \n" +" click here to review \n" +"
    \n" +"
    \n" +" " +msgstr "" +"\n" +"
    \n" +"

    你有一个新工单

    \n" +"
    \n" +" {body}\n" +"
    \n" +" 点击我查看 \n" +"
    \n" +"
    \n" +" " + +#: tickets/utils.py:40 +msgid "Ticket has been reply" +msgstr "工单已被回复" + +#: tickets/utils.py:41 +#, python-brace-format +msgid "" +"\n" +"
    \n" +"

    Your ticket has been replay

    \n" +"
    \n" +" Title: {ticket.title}\n" +"
    \n" +" Assignee: {ticket.assignee_display}\n" +"
    \n" +" Status: {ticket.status_display}\n" +"
    \n" +"
    \n" +"
    \n" +" " +msgstr "" +"\n" +"
    \n" +"

    您的工单已被回复

    \n" +"
    \n" +" 标题: {ticket.title}\n" +"
    \n" +" 处理人: {ticket.assignee_display}\n" +"
    \n" +" 状态: {ticket.status_display}\n" +"
    \n" +"
    \n" +"
    \n" +" " + +#: tickets/views.py:20 +msgid "Ticket list" +msgstr "工单列表" + +#: tickets/views.py:38 +msgid "Ticket detail" +msgstr "工单详情" + +#: users/api/user.py:177 +msgid "Could not reset self otp, use profile reset instead" +msgstr "不能再该页面重置MFA, 请去个人信息页面重置" + +#: users/forms/group.py:19 users/forms/user.py:143 users/forms/user.py:148 +#: xpack/plugins/orgs/forms.py:17 +msgid "Select users" +msgstr "选择用户" + +#: users/forms/profile.py:37 msgid "" "When enabled, you will enter the MFA binding process the next time you log " "in. you can also directly bind in \"personal information -> quick " @@ -4616,11 +5042,11 @@ msgstr "" "启用之后您将会在下次登录时进入MFA绑定流程;您也可以在(个人信息->快速修改->更" "改MFA设置)中直接绑定!" -#: users/forms.py:189 +#: users/forms/profile.py:47 msgid "* Enable MFA authentication to make the account more secure." msgstr "* 启用MFA认证,使账号更加安全。" -#: users/forms.py:199 +#: users/forms/profile.py:57 msgid "" "In order to protect you and your company, please keep your account, password " "and key sensitive information properly. (for example: setting complex " @@ -4629,88 +5055,139 @@ msgstr "" "为了保护您和公司的安全,请妥善保管您的账户、密码和密钥等重要敏感信息;(如:" "设置复杂密码,启用MFA认证)" -#: users/forms.py:206 users/templates/users/first_login.html:48 +#: users/forms/profile.py:64 users/templates/users/first_login.html:48 #: users/templates/users/first_login.html:110 #: users/templates/users/first_login.html:139 msgid "Finish" msgstr "完成" -#: users/forms.py:212 -msgid "Old password" -msgstr "原来密码" - -#: users/forms.py:217 +#: users/forms/profile.py:71 msgid "New password" msgstr "新密码" -#: users/forms.py:222 +#: users/forms/profile.py:76 msgid "Confirm password" msgstr "确认密码" -#: users/forms.py:232 -msgid "Old password error" -msgstr "原来密码错误" - -#: users/forms.py:240 +#: users/forms/profile.py:84 msgid "Password does not match" msgstr "密码不一致" -#: users/forms.py:250 +#: users/forms/profile.py:96 +msgid "Old password" +msgstr "原来密码" + +#: users/forms/profile.py:106 +msgid "Old password error" +msgstr "原来密码错误" + +#: users/forms/profile.py:116 msgid "Automatically configure and download the SSH key" msgstr "自动配置并下载SSH密钥" -#: users/forms.py:254 +#: users/forms/profile.py:118 users/forms/user.py:34 +#: users/templates/users/user_update.html:30 +msgid "ssh public key" +msgstr "ssh公钥" + +#: users/forms/profile.py:119 users/forms/user.py:35 +msgid "ssh-rsa AAAA..." +msgstr "" + +#: users/forms/profile.py:120 msgid "Paste your id_rsa.pub here." msgstr "复制你的公钥到这里" -#: users/forms.py:288 users/forms.py:293 users/forms.py:343 -#: xpack/plugins/orgs/forms.py:18 -msgid "Select users" -msgstr "选择用户" +#: users/forms/profile.py:133 users/forms/user.py:86 +msgid "Public key should not be the same as your old one." +msgstr "不能和原来的密钥相同" -#: users/models/user.py:50 users/templates/users/user_update.html:22 -#: users/views/login.py:46 users/views/login.py:107 -msgid "User auth from {}, go there change password" -msgstr "用户认证源来自 {}, 请去相应系统修改密码" +#: users/forms/profile.py:137 users/forms/user.py:90 +#: users/serializers/user.py:122 +msgid "Not a valid ssh public key" +msgstr "ssh密钥不合法" + +#: users/forms/user.py:27 users/models/user.py:448 +#: users/templates/users/_select_user_modal.html:15 +#: users/templates/users/user_detail.html:73 +#: users/templates/users/user_list.html:16 +#: users/templates/users/user_profile.html:55 +msgid "Role" +msgstr "角色" + +#: users/forms/user.py:31 users/models/user.py:483 +#: users/templates/users/user_detail.html:89 +#: users/templates/users/user_list.html:18 +#: users/templates/users/user_profile.html:102 +msgid "Source" +msgstr "用户来源" + +#: users/forms/user.py:36 +msgid "Paste user id_rsa.pub here." +msgstr "复制用户公钥到这里" + +#: users/forms/user.py:51 users/templates/users/user_detail.html:217 +msgid "Join user groups" +msgstr "添加到用户组" + +#: users/forms/user.py:103 users/views/login.py:119 users/views/profile.py:107 +msgid "* Your password does not meet the requirements" +msgstr "* 您的密码不符合要求" + +#: users/forms/user.py:124 +msgid "Reset link will be generated and sent to the user" +msgstr "生成重置密码链接,通过邮件发送给用户" + +#: users/forms/user.py:125 +msgid "Set password" +msgstr "设置密码" + +#: users/forms/user.py:132 xpack/plugins/change_auth_plan/models.py:88 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:45 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:67 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:57 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:16 +msgid "Password strategy" +msgstr "密码策略" -#: users/models/user.py:126 users/models/user.py:508 +#: users/models/user.py:142 users/models/user.py:576 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:128 -msgid "Application" -msgstr "应用程序" - -#: users/models/user.py:129 xpack/plugins/orgs/forms.py:30 +#: users/models/user.py:145 xpack/plugins/orgs/forms.py:29 #: xpack/plugins/orgs/templates/orgs/org_list.html:14 msgid "Auditor" msgstr "审计员" -#: users/models/user.py:139 +#: users/models/user.py:155 msgid "Org admin" msgstr "组织管理员" -#: users/models/user.py:141 +#: users/models/user.py:157 msgid "Org auditor" msgstr "组织审计员" -#: users/models/user.py:332 users/templates/users/user_profile.html:90 +#: users/models/user.py:362 users/templates/users/user_profile.html:90 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:386 +#: users/models/user.py:428 +msgid "Local" +msgstr "数据库" + +#: users/models/user.py:451 msgid "Avatar" msgstr "头像" -#: users/models/user.py:389 users/templates/users/user_detail.html:82 +#: users/models/user.py:454 users/templates/users/user_detail.html:68 msgid "Wechat" msgstr "微信" -#: users/models/user.py:422 +#: users/models/user.py:487 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:511 +#: users/models/user.py:579 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" @@ -4718,63 +5195,59 @@ msgstr "Administrator是初始的超级管理员" msgid "Auditors cannot be join in the user group" msgstr "审计员不能被加入到用户组" -#: users/serializers/user.py:40 -msgid "Groups name" -msgstr "用户组名" - -#: users/serializers/user.py:41 -msgid "Source name" -msgstr "用户来源名" - -#: users/serializers/user.py:42 +#: users/serializers/user.py:35 msgid "Is first login" msgstr "首次登录" -#: users/serializers/user.py:43 -msgid "Role name" -msgstr "角色名" - -#: users/serializers/user.py:44 +#: users/serializers/user.py:36 msgid "Is valid" msgstr "账户是否有效" -#: users/serializers/user.py:45 +#: users/serializers/user.py:37 msgid "Is expired" msgstr " 是否过期" -#: users/serializers/user.py:46 +#: users/serializers/user.py:38 msgid "Avatar url" msgstr "头像路径" -#: users/serializers/user.py:66 +#: users/serializers/user.py:46 msgid "Role limit to {}" msgstr "角色只能为 {}" -#: users/serializers/user.py:78 +#: users/serializers/user.py:58 msgid "Password does not match security rules" msgstr "密码不满足安全规则" +#: users/serializers/user.py:107 +msgid "Groups name" +msgstr "用户组名" + +#: users/serializers/user.py:108 +msgid "Source name" +msgstr "用户来源名" + +#: users/serializers/user.py:109 +msgid "Role name" +msgstr "角色名" + #: users/serializers_v2/user.py:36 msgid "name not unique" msgstr "名称重复" -#: users/templates/users/_base_otp.html:27 -msgid "Home page" -msgstr "首页" - -#: users/templates/users/_base_otp.html:44 +#: users/templates/users/_base_otp.html:14 msgid "Security token validation" msgstr "安全令牌验证" -#: users/templates/users/_base_otp.html:44 users/templates/users/_user.html:13 +#: users/templates/users/_base_otp.html:14 users/templates/users/_user.html:13 #: users/templates/users/user_profile_update.html:55 -#: xpack/plugins/cloud/models.py:147 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:60 +#: xpack/plugins/cloud/models.py:146 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:57 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:13 msgid "Account" msgstr "账户" -#: users/templates/users/_base_otp.html:44 +#: users/templates/users/_base_otp.html:14 msgid "Follow these steps to complete the binding operation" msgstr "请按照以下步骤完成绑定操作" @@ -4790,23 +5263,28 @@ msgstr "资产数量" msgid "Security and Role" msgstr "角色安全" -#: users/templates/users/_user_groups_import_modal.html:4 -msgid "Import user groups" -msgstr "导入用户组" +#: users/templates/users/_user_detail_nav_header.html:11 +#: users/views/user.py:179 +msgid "User detail" +msgstr "用户详情" -#: users/templates/users/_user_groups_update_modal.html:4 -#: users/views/group.py:64 -msgid "Update user group" -msgstr "更新用户组" +#: users/templates/users/_user_detail_nav_header.html:15 +msgid "User permissions" +msgstr "用户授权" -#: users/templates/users/_user_import_modal.html:4 -msgid "Import users" -msgstr "导入用户" +#: users/templates/users/_user_detail_nav_header.html:23 +#: users/templates/users/user_group_detail.html:20 +#: users/templates/users/user_group_granted_asset.html:21 +msgid "Asset granted" +msgstr "授权的资产" -#: users/templates/users/_user_update_modal.html:4 -#: users/templates/users/user_update.html:4 users/views/user.py:123 -msgid "Update user" -msgstr "更新用户" +#: users/templates/users/_user_detail_nav_header.html:40 +msgid "RemoteApp granted" +msgstr "授权的远程应用" + +#: users/templates/users/_user_detail_nav_header.html:54 +msgid "DatabaseApp granted" +msgstr "授权的数据库应用" #: users/templates/users/_user_update_pk_modal.html:4 msgid "Update User SSH Public Key" @@ -4846,100 +5324,66 @@ msgstr "向导" msgid " for more information" msgstr "获取更多信息" -#: users/templates/users/forgot_password.html:31 +#: users/templates/users/forgot_password.html:20 msgid "Input your email, that will send a mail to your" msgstr "输入您的邮箱, 将会发一封重置邮件到您的邮箱中" -#: users/templates/users/reset_password.html:28 -msgid "" -"Jumpserver is an open source desktop system developed using Python and " -"Django that helps Internet businesses with efficient users, assets, " -"permissions, and audit management" -msgstr "" -"Jumpserver是一款使用Python, Django开发的开源跳板机系统, 助力互联网企业高效 用" -"户、资产、权限、审计 管理" - -#: users/templates/users/reset_password.html:32 -msgid "" -"We are from all over the world, we have great admiration and worship for the " -"spirit of open source, we have endless pursuit for perfection, neatness and " -"elegance" -msgstr "" -"我们自五湖四海,我们对开源精神无比敬仰和崇拜,我们对完美、整洁、优雅 无止境的" -"追求" - -#: users/templates/users/reset_password.html:36 -msgid "" -"We focus on automatic operation and maintenance, and strive to build an easy-" -"to-use, stable, safe and automatic board hopping machine, which is our " -"unremitting pursuit and power" -msgstr "" -"专注自动化运维,努力打造 易用、稳定、安全、自动化 的跳板机, 这是我们的不懈的" -"追求和动力" - -#: users/templates/users/reset_password.html:40 -msgid "Always young, always with tears in my eyes. Stay foolish Stay hungry" -msgstr "永远年轻,永远热泪盈眶 stay foolish stay hungry" - -#: users/templates/users/reset_password.html:46 -#: users/templates/users/user_detail.html:379 users/utils.py:83 +#: users/templates/users/reset_password.html:5 +#: users/templates/users/reset_password.html:6 +#: users/templates/users/user_detail.html:402 users/utils.py:83 msgid "Reset password" msgstr "重置密码" -#: users/templates/users/reset_password.html:59 +#: users/templates/users/reset_password.html:23 #: users/templates/users/user_create.html:13 #: users/templates/users/user_password_update.html:65 #: users/templates/users/user_update.html:13 msgid "Your password must satisfy" msgstr "您的密码必须满足:" -#: users/templates/users/reset_password.html:60 +#: users/templates/users/reset_password.html:24 #: users/templates/users/user_create.html:14 #: users/templates/users/user_password_update.html:66 #: users/templates/users/user_update.html:14 msgid "Password strength" msgstr "密码强度:" -#: users/templates/users/reset_password.html:66 -msgid "Password again" -msgstr "再次输入密码" - -#: users/templates/users/reset_password.html:105 +#: users/templates/users/reset_password.html:48 #: users/templates/users/user_create.html:33 #: users/templates/users/user_password_update.html:103 #: users/templates/users/user_update.html:55 msgid "Very weak" msgstr "很弱" -#: users/templates/users/reset_password.html:106 +#: users/templates/users/reset_password.html:49 #: users/templates/users/user_create.html:34 #: users/templates/users/user_password_update.html:104 #: users/templates/users/user_update.html:56 msgid "Weak" msgstr "弱" -#: users/templates/users/reset_password.html:107 +#: users/templates/users/reset_password.html:50 #: users/templates/users/user_create.html:35 #: users/templates/users/user_password_update.html:105 #: users/templates/users/user_update.html:57 msgid "Normal" msgstr "正常" -#: users/templates/users/reset_password.html:108 +#: users/templates/users/reset_password.html:51 #: users/templates/users/user_create.html:36 #: users/templates/users/user_password_update.html:106 #: users/templates/users/user_update.html:58 msgid "Medium" msgstr "一般" -#: users/templates/users/reset_password.html:109 +#: users/templates/users/reset_password.html:52 #: users/templates/users/user_create.html:37 #: users/templates/users/user_password_update.html:107 #: users/templates/users/user_update.html:59 msgid "Strong" msgstr "强" -#: users/templates/users/reset_password.html:110 +#: users/templates/users/reset_password.html:53 #: users/templates/users/user_create.html:38 #: users/templates/users/user_password_update.html:108 #: users/templates/users/user_update.html:60 @@ -4947,175 +5391,176 @@ msgid "Very strong" msgstr "很强" #: users/templates/users/user_create.html:4 -#: users/templates/users/user_list.html:28 users/views/user.py:79 +#: users/templates/users/user_list.html:7 users/views/user.py:68 msgid "Create user" msgstr "创建用户" -#: users/templates/users/user_detail.html:19 -#: users/templates/users/user_granted_asset.html:18 users/views/user.py:190 -msgid "User detail" -msgstr "用户详情" - -#: users/templates/users/user_detail.html:22 -#: users/templates/users/user_granted_asset.html:21 -#: users/templates/users/user_group_detail.html:25 -#: users/templates/users/user_group_granted_asset.html:21 -msgid "Asset granted" -msgstr "授权的资产" - -#: users/templates/users/user_detail.html:94 +#: users/templates/users/user_detail.html:80 msgid "Force enabled" msgstr "强制启用" -#: users/templates/users/user_detail.html:119 +#: users/templates/users/user_detail.html:105 #: users/templates/users/user_profile.html:110 msgid "Last login" msgstr "最后登录" -#: users/templates/users/user_detail.html:124 +#: users/templates/users/user_detail.html:110 #: users/templates/users/user_profile.html:115 msgid "Last password updated" msgstr "最后更新密码" -#: users/templates/users/user_detail.html:160 +#: users/templates/users/user_detail.html:148 msgid "Force enabled MFA" msgstr "强制启用MFA" -#: users/templates/users/user_detail.html:175 +#: users/templates/users/user_detail.html:165 msgid "Reset MFA" msgstr "重置MFA" -#: users/templates/users/user_detail.html:184 +#: users/templates/users/user_detail.html:174 msgid "Send reset password mail" msgstr "发送重置密码邮件" +#: users/templates/users/user_detail.html:177 #: users/templates/users/user_detail.html:187 -#: users/templates/users/user_detail.html:197 msgid "Send" msgstr "发送" -#: users/templates/users/user_detail.html:194 +#: users/templates/users/user_detail.html:184 msgid "Send reset ssh key mail" msgstr "发送重置密钥邮件" -#: users/templates/users/user_detail.html:203 -#: users/templates/users/user_detail.html:467 +#: users/templates/users/user_detail.html:193 +#: users/templates/users/user_detail.html:490 msgid "Unblock user" msgstr "解除登录限制" -#: users/templates/users/user_detail.html:206 +#: users/templates/users/user_detail.html:196 msgid "Unblock" msgstr "解除" -#: users/templates/users/user_detail.html:322 +#: users/templates/users/user_detail.html:365 msgid "Goto profile page enable MFA" msgstr "请去个人信息页面启用自己的MFA" -#: users/templates/users/user_detail.html:378 +#: users/templates/users/user_detail.html:401 msgid "An e-mail has been sent to the user`s mailbox." msgstr "已发送邮件到用户邮箱" -#: users/templates/users/user_detail.html:389 +#: users/templates/users/user_detail.html:412 msgid "This will reset the user password and send a reset mail" msgstr "将失效用户当前密码,并发送重设密码邮件到用户邮箱" -#: users/templates/users/user_detail.html:404 +#: users/templates/users/user_detail.html:427 msgid "" "The reset-ssh-public-key E-mail has been sent successfully. Please inform " "the user to update his new ssh public key." msgstr "重设密钥邮件将会发送到用户邮箱" -#: users/templates/users/user_detail.html:405 +#: users/templates/users/user_detail.html:428 msgid "Reset SSH public key" msgstr "重置SSH密钥" -#: users/templates/users/user_detail.html:415 +#: users/templates/users/user_detail.html:438 msgid "This will reset the user public key and send a reset mail" msgstr "将会失效用户当前密钥,并发送重置邮件到用户邮箱" -#: users/templates/users/user_detail.html:433 +#: users/templates/users/user_detail.html:456 msgid "Successfully updated the SSH public key." msgstr "更新ssh密钥成功" -#: users/templates/users/user_detail.html:434 -#: users/templates/users/user_detail.html:438 +#: users/templates/users/user_detail.html:457 +#: users/templates/users/user_detail.html:461 msgid "User SSH public key update" msgstr "ssh密钥" -#: users/templates/users/user_detail.html:483 +#: users/templates/users/user_detail.html:506 msgid "After unlocking the user, the user can log in normally." msgstr "解除用户登录限制后,此用户即可正常登录" -#: users/templates/users/user_detail.html:497 +#: users/templates/users/user_detail.html:520 msgid "Reset user MFA success" msgstr "重置用户MFA成功" -#: users/templates/users/user_group_detail.html:22 +#: users/templates/users/user_disable_mfa.html:6 +#: users/templates/users/user_password_check.html:6 +msgid "Authenticate" +msgstr "验证身份" + +#: users/templates/users/user_disable_mfa.html:32 +msgid "Unbind" +msgstr "解绑 MFA" + +#: users/templates/users/user_group_detail.html:17 #: users/templates/users/user_group_granted_asset.html:18 #: users/views/group.py:83 msgid "User group detail" msgstr "用户组详情" -#: users/templates/users/user_group_detail.html:86 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:121 +#: users/templates/users/user_group_detail.html:81 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:116 msgid "Add user" msgstr "添加用户" -#: users/templates/users/user_group_list.html:28 users/views/group.py:46 +#: users/templates/users/user_group_list.html:7 users/views/group.py:46 msgid "Create user group" msgstr "创建用户组" -#: users/templates/users/user_group_list.html:115 -msgid "This will delete the selected groups !!!" -msgstr "删除选择组" - -#: users/templates/users/user_group_list.html:124 -msgid "UserGroups Deleted." -msgstr "用户组删除" +#: users/templates/users/user_group_list.html:90 +#: users/templates/users/user_list.html:135 +#: users/templates/users/user_profile.html:124 +msgid "User groups" +msgstr "用户组" -#: users/templates/users/user_group_list.html:125 -#: users/templates/users/user_group_list.html:130 -msgid "UserGroups Delete" -msgstr "用户组删除" +#: users/templates/users/user_list.html:32 +msgid "Remove selected" +msgstr "批量移除" -#: users/templates/users/user_group_list.html:129 -msgid "UserGroup Deleting failed." -msgstr "用户组删除失败" +#: users/templates/users/user_list.html:106 +#: users/templates/users/user_list.html:110 +msgid "Remove" +msgstr "移除" -#: users/templates/users/user_list.html:251 +#: users/templates/users/user_list.html:179 msgid "This will delete the selected users !!!" msgstr "删除选中用户 !!!" -#: users/templates/users/user_list.html:262 -msgid "User Deleted." -msgstr "已被删除" +#: users/templates/users/user_list.html:190 +msgid "User Deleting failed." +msgstr "用户删除失败" -#: users/templates/users/user_list.html:263 -#: users/templates/users/user_list.html:267 +#: users/templates/users/user_list.html:191 msgid "User Delete" msgstr "删除" +#: users/templates/users/user_list.html:213 +msgid "This will remove the selected users !!" +msgstr "移除选中用户 !!!" + +#: users/templates/users/user_list.html:215 +msgid "User Removing failed." +msgstr "用户移除失败" + +#: users/templates/users/user_list.html:216 +msgid "User Remove" +msgstr "移除" + +#: users/templates/users/user_list.html:265 +msgid "Are you sure about removing it?" +msgstr "您确定移除吗?" + #: users/templates/users/user_list.html:266 -msgid "User Deleting failed." -msgstr "用户删除失败" +msgid "Remove the success" +msgstr "移除成功" -#: users/templates/users/user_list.html:327 +#: users/templates/users/user_list.html:271 msgid "User is expired" msgstr "用户已失效" -#: users/templates/users/user_list.html:330 +#: users/templates/users/user_list.html:274 msgid "User is inactive" msgstr "用户已禁用" -#: users/templates/users/user_otp_authentication.html:6 -#: users/templates/users/user_password_authentication.html:6 -msgid "Authenticate" -msgstr "验证身份" - -#: users/templates/users/user_otp_authentication.html:32 -msgid "Unbind" -msgstr "解绑 MFA" - #: users/templates/users/user_otp_enable_bind.html:6 msgid "Bind" msgstr "绑定 MFA" @@ -5142,7 +5587,7 @@ msgstr "Android手机下载" msgid "iPhone downloads" msgstr "iPhone手机下载" -#: users/templates/users/user_otp_enable_install_app.html:23 +#: users/templates/users/user_otp_enable_install_app.html:22 msgid "" "After installation, click the next step to enter the binding page (if " "installed, go to the next step directly)." @@ -5152,22 +5597,18 @@ msgstr "安装完成后点击下一步进入绑定页面(如已安装,直接 msgid "Administrator Settings force MFA login" msgstr "管理员设置强制使用MFA登录" -#: users/templates/users/user_profile.html:124 -msgid "User groups" -msgstr "用户组" - #: users/templates/users/user_profile.html:156 msgid "Set MFA" msgstr "设置MFA" #: users/templates/users/user_profile.html:178 -msgid "Update password" -msgstr "更改密码" - -#: users/templates/users/user_profile.html:188 msgid "Update MFA" msgstr "更改MFA" +#: users/templates/users/user_profile.html:188 +msgid "Update password" +msgstr "更改密码" + #: users/templates/users/user_profile.html:198 msgid "Update SSH public key" msgstr "更改SSH密钥" @@ -5198,6 +5639,15 @@ msgid "" "corresponding private key." msgstr "新的公钥已设置成功,请下载对应的私钥" +#: users/templates/users/user_update.html:4 users/views/user.py:112 +msgid "Update user" +msgstr "更新用户" + +#: users/templates/users/user_update.html:22 users/views/login.py:48 +#: users/views/login.py:113 +msgid "User auth from {}, go there change password" +msgstr "用户认证源来自 {}, 请去相应系统修改密码" + # msgid "Update user" # msgstr "更新用户" #: users/utils.py:24 @@ -5404,102 +5854,110 @@ msgstr "" msgid "User group list" msgstr "用户组列表" +#: users/views/group.py:64 +msgid "Update user group" +msgstr "更新用户组" + #: users/views/group.py:100 msgid "User group granted asset" msgstr "用户组授权资产" -#: users/views/login.py:43 +#: users/views/login.py:45 msgid "Email address invalid, please input again" msgstr "邮箱地址错误,重新输入" -#: users/views/login.py:59 +#: users/views/login.py:61 msgid "Send reset password message" msgstr "发送重置密码邮件" -#: users/views/login.py:60 +#: users/views/login.py:62 msgid "Send reset password mail success, login your mail box and follow it " msgstr "" "发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)" -#: users/views/login.py:73 +#: users/views/login.py:75 msgid "Reset password success" msgstr "重置密码成功" -#: users/views/login.py:74 +#: users/views/login.py:76 msgid "Reset password success, return to login page" msgstr "重置密码成功,返回到登录页面" -#: users/views/login.py:89 users/views/login.py:105 +#: users/views/login.py:100 users/views/login.py:110 msgid "Token invalid or expired" msgstr "Token错误或失效" -#: users/views/login.py:101 -msgid "Password not same" -msgstr "密码不一致" - -#: users/views/login.py:154 +#: users/views/login.py:158 msgid "First login" msgstr "首次登录" -#: users/views/user.py:141 -msgid "Bulk update user success" -msgstr "批量更新用户成功" - -#: users/views/user.py:169 -msgid "Bulk update user" -msgstr "批量更新用户" - -#: users/views/user.py:218 -msgid "User granted assets" -msgstr "用户授权资产" - -#: users/views/user.py:251 +#: users/views/profile.py:71 msgid "Profile setting" msgstr "个人信息设置" -#: users/views/user.py:271 +#: users/views/profile.py:91 msgid "Password update" msgstr "密码更新" -#: users/views/user.py:306 +#: users/views/profile.py:126 msgid "Public key update" msgstr "密钥更新" -#: users/views/user.py:348 +#: users/views/profile.py:154 msgid "Password invalid" msgstr "用户名或密码无效" -#: users/views/user.py:448 +#: users/views/profile.py:265 msgid "MFA enable success" msgstr "MFA 绑定成功" -#: users/views/user.py:449 +#: users/views/profile.py:266 msgid "MFA enable success, return login page" msgstr "MFA 绑定成功,返回到登录页面" -#: users/views/user.py:451 +#: users/views/profile.py:268 msgid "MFA disable success" msgstr "MFA 解绑成功" -#: users/views/user.py:452 +#: users/views/profile.py:269 msgid "MFA disable success, return login page" msgstr "MFA 解绑成功,返回登录页面" +#: users/views/user.py:130 +msgid "Bulk update user success" +msgstr "批量更新用户成功" + +#: users/views/user.py:158 +msgid "Bulk update user" +msgstr "批量更新用户" + +#: users/views/user.py:207 +msgid "User granted assets" +msgstr "用户授权资产" + +#: users/views/user.py:235 +msgid "User granted RemoteApp" +msgstr "用户授权远程应用" + +#: users/views/user.py:263 +msgid "User granted DatabaseApp" +msgstr "用户授权数据库应用" + #: xpack/plugins/change_auth_plan/forms.py:20 msgid "Password length" msgstr "密码长度" #: xpack/plugins/change_auth_plan/forms.py:75 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:60 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:81 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:54 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:79 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:17 #: xpack/plugins/cloud/forms.py:33 xpack/plugins/cloud/forms.py:87 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:41 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:72 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:37 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:69 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:16 #: xpack/plugins/gathered_user/forms.py:13 #: xpack/plugins/gathered_user/forms.py:41 -#: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:32 +#: xpack/plugins/gathered_user/templates/gathered_user/task_create_update.html:28 msgid "Periodic perform" msgstr "定时执行" @@ -5555,9 +6013,9 @@ msgstr "所有资产使用不同的随机密码" #: xpack/plugins/change_auth_plan/models.py:78 #: xpack/plugins/change_auth_plan/models.py:147 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:100 -#: xpack/plugins/cloud/models.py:165 xpack/plugins/cloud/models.py:219 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:91 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:98 +#: xpack/plugins/cloud/models.py:164 xpack/plugins/cloud/models.py:218 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:88 #: xpack/plugins/gathered_user/models.py:35 #: xpack/plugins/gathered_user/models.py:72 msgid "Cycle perform" @@ -5565,16 +6023,16 @@ msgstr "周期执行" #: xpack/plugins/change_auth_plan/models.py:83 #: xpack/plugins/change_auth_plan/models.py:145 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:92 -#: xpack/plugins/cloud/models.py:170 xpack/plugins/cloud/models.py:217 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:83 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:90 +#: xpack/plugins/cloud/models.py:169 xpack/plugins/cloud/models.py:216 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:80 #: xpack/plugins/gathered_user/models.py:40 #: xpack/plugins/gathered_user/models.py:70 msgid "Regularly perform" msgstr "定期执行" #: xpack/plugins/change_auth_plan/models.py:92 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:74 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:72 msgid "Password rules" msgstr "密码规则" @@ -5591,19 +6049,19 @@ msgid "Change auth plan snapshot" msgstr "改密计划快照" #: xpack/plugins/change_auth_plan/models.py:275 -#: xpack/plugins/change_auth_plan/models.py:426 +#: xpack/plugins/change_auth_plan/models.py:432 msgid "Change auth plan execution" msgstr "改密计划执行" -#: xpack/plugins/change_auth_plan/models.py:435 +#: xpack/plugins/change_auth_plan/models.py:441 msgid "Change auth plan execution subtask" msgstr "改密计划执行子任务" -#: xpack/plugins/change_auth_plan/models.py:453 +#: xpack/plugins/change_auth_plan/models.py:459 msgid "Authentication failed" msgstr "认证失败" -#: xpack/plugins/change_auth_plan/models.py:455 +#: xpack/plugins/change_auth_plan/models.py:461 msgid "Connection timeout" msgstr "连接超时" @@ -5628,46 +6086,36 @@ msgstr "* 密码长度范围 6-30 位" msgid "* Please enter a valid crontab expression" msgstr "* 请输入有效的 crontab 表达式" -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:23 -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:26 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:19 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:24 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:23 #: xpack/plugins/change_auth_plan/views.py:133 msgid "Plan execution list" msgstr "执行列表" -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:66 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:62 msgid "Add asset to this plan" msgstr "添加资产" -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:91 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_asset_list.html:87 msgid "Add node to this plan" msgstr "添加节点" -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:12 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:7 msgid "" "When the user password on the asset is changed, the action is performed " "using the admin user associated with the asset" msgstr "更改资产上的用户密码时,将会使用与该资产关联的管理用户进行操作" -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:76 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:74 msgid "Length" msgstr "长度" -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:84 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:75 -msgid "Yes" -msgstr "是" - -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:86 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:77 -msgid "No" -msgstr "否" - -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:134 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:132 msgid "Run plan manually" msgstr "手动执行计划" -#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:178 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:176 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:102 #: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:90 msgid "Execute failed" @@ -5678,7 +6126,7 @@ msgid "Execution list of plan" msgstr "执行列表" #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:104 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:89 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:84 msgid "Log" msgstr "日志" @@ -5735,7 +6183,7 @@ msgstr "选择实例" msgid "Select node" msgstr "选择节点" -#: xpack/plugins/cloud/forms.py:82 xpack/plugins/orgs/forms.py:21 +#: xpack/plugins/cloud/forms.py:82 xpack/plugins/orgs/forms.py:20 msgid "Select admins" msgstr "选择管理员" @@ -5748,80 +6196,80 @@ msgstr "选择管理员" msgid "Cloud center" msgstr "云管中心" -#: xpack/plugins/cloud/models.py:53 +#: xpack/plugins/cloud/models.py:52 msgid "Available" msgstr "有效" -#: xpack/plugins/cloud/models.py:54 +#: xpack/plugins/cloud/models.py:53 msgid "Unavailable" msgstr "无效" -#: xpack/plugins/cloud/models.py:63 -#: xpack/plugins/cloud/templates/cloud/account_detail.html:54 +#: xpack/plugins/cloud/models.py:62 +#: xpack/plugins/cloud/templates/cloud/account_detail.html:51 #: xpack/plugins/cloud/templates/cloud/account_list.html:13 msgid "Provider" msgstr "云服务商" -#: xpack/plugins/cloud/models.py:66 +#: xpack/plugins/cloud/models.py:65 msgid "Access key id" msgstr "" -#: xpack/plugins/cloud/models.py:70 +#: xpack/plugins/cloud/models.py:69 msgid "Access key secret" msgstr "" -#: xpack/plugins/cloud/models.py:88 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:30 +#: xpack/plugins/cloud/models.py:87 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:26 msgid "Cloud account" msgstr "云账号" -#: xpack/plugins/cloud/models.py:150 +#: xpack/plugins/cloud/models.py:149 msgid "Regions" msgstr "地域" -#: xpack/plugins/cloud/models.py:153 +#: xpack/plugins/cloud/models.py:152 msgid "Instances" msgstr "实例" -#: xpack/plugins/cloud/models.py:176 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:97 +#: xpack/plugins/cloud/models.py:175 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:94 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:17 msgid "Date last sync" msgstr "最后同步日期" -#: xpack/plugins/cloud/models.py:187 xpack/plugins/cloud/models.py:271 +#: xpack/plugins/cloud/models.py:186 xpack/plugins/cloud/models.py:270 msgid "Sync instance task" msgstr "同步实例任务" -#: xpack/plugins/cloud/models.py:265 xpack/plugins/cloud/models.py:288 +#: xpack/plugins/cloud/models.py:264 xpack/plugins/cloud/models.py:287 msgid "Succeed" msgstr "成功" -#: xpack/plugins/cloud/models.py:266 +#: xpack/plugins/cloud/models.py:265 msgid "Partial succeed" msgstr "" -#: xpack/plugins/cloud/models.py:281 xpack/plugins/cloud/models.py:313 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:71 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:66 +#: xpack/plugins/cloud/models.py:280 xpack/plugins/cloud/models.py:312 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:66 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:63 msgid "Date sync" msgstr "同步日期" -#: xpack/plugins/cloud/models.py:289 +#: xpack/plugins/cloud/models.py:288 msgid "Exist" msgstr "存在" -#: xpack/plugins/cloud/models.py:294 +#: xpack/plugins/cloud/models.py:293 msgid "Sync task" msgstr "同步任务" -#: xpack/plugins/cloud/models.py:298 +#: xpack/plugins/cloud/models.py:297 msgid "Sync instance task history" msgstr "同步实例任务历史" -#: xpack/plugins/cloud/models.py:301 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:117 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:61 +#: xpack/plugins/cloud/models.py:300 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:114 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:58 msgid "Instance" msgstr "实例" @@ -5841,7 +6289,7 @@ msgstr "AWS (国际)" msgid "Qcloud" msgstr "腾讯云" -#: xpack/plugins/cloud/templates/cloud/account_detail.html:20 +#: xpack/plugins/cloud/templates/cloud/account_detail.html:17 #: xpack/plugins/cloud/views.py:79 msgid "Account detail" msgstr "账户详情" @@ -5851,61 +6299,61 @@ msgstr "账户详情" msgid "Create account" msgstr "创建账户" -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:33 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:29 msgid "Region & Instance" msgstr "地域 & 实例" -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:37 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:33 msgid "Node & AdminUser" msgstr "节点 & 管理用户" -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:67 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create_update.html:63 msgid "Load failed" msgstr "加载失败" -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:20 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:25 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:21 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:17 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:20 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:18 #: xpack/plugins/cloud/views.py:144 msgid "Sync task detail" msgstr "同步任务详情" -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:23 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:28 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:24 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:20 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:23 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:21 #: xpack/plugins/cloud/views.py:160 msgid "Sync task history" msgstr "同步历史列表" -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:26 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:31 -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:27 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:23 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:26 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:24 #: xpack/plugins/cloud/views.py:212 msgid "Sync instance list" msgstr "同步实例列表" -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:138 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:135 msgid "Run task manually" msgstr "手动执行任务" -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:181 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:178 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:99 msgid "Sync success" msgstr "同步成功" -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:65 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:60 msgid "Total count" msgstr "总数" -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:66 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:61 msgid "Succeed count" msgstr "成功" -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:67 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:62 msgid "Failed count" msgstr "失败" -#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:68 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:63 msgid "Exist count" msgstr "存在" @@ -5953,22 +6401,26 @@ msgid "Periodic" msgstr "定时执行" #: xpack/plugins/gathered_user/models.py:57 -#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:48 +#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:13 msgid "Gather user task" msgstr "收集用户任务" -#: xpack/plugins/gathered_user/models.py:140 +#: xpack/plugins/gathered_user/models.py:137 msgid "Task" msgstr "任务" -#: xpack/plugins/gathered_user/models.py:152 +#: xpack/plugins/gathered_user/models.py:149 msgid "gather user task execution" msgstr "收集用户执行" -#: xpack/plugins/gathered_user/models.py:158 +#: xpack/plugins/gathered_user/models.py:155 msgid "Assets is empty, please change nodes" msgstr "资产为空,请更改节点" +#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:170 +msgid "Asset user" +msgstr "资产用户" + #: xpack/plugins/gathered_user/templates/gathered_user/task_list.html:7 #: xpack/plugins/gathered_user/views.py:50 msgid "Create task" @@ -6063,8 +6515,8 @@ msgid "It is already in the default setting state!" msgstr "当前已经是初始化状态!" #: xpack/plugins/license/meta.py:11 xpack/plugins/license/models.py:94 -#: xpack/plugins/license/templates/license/license_detail.html:50 -#: xpack/plugins/license/templates/license/license_detail.html:55 +#: xpack/plugins/license/templates/license/license_detail.html:41 +#: xpack/plugins/license/templates/license/license_detail.html:46 #: xpack/plugins/license/views.py:32 msgid "License" msgstr "许可证" @@ -6077,8 +6529,12 @@ msgstr "标准版" msgid "Enterprise edition" msgstr "企业版" +#: xpack/plugins/license/models.py:78 +msgid "Ultimate edition" +msgstr "旗舰版" + #: xpack/plugins/license/templates/license/_license_import_modal.html:4 -#: xpack/plugins/license/templates/license/license_detail.html:108 +#: xpack/plugins/license/templates/license/license_detail.html:86 msgid "Import license" msgstr "导入许可证" @@ -6086,64 +6542,53 @@ msgstr "导入许可证" msgid "License file" msgstr "许可证文件" -#: xpack/plugins/license/templates/license/license_detail.html:12 +#: xpack/plugins/license/templates/license/license_detail.html:11 msgid "Please Import License" msgstr "请导入许可证" -#: xpack/plugins/license/templates/license/license_detail.html:17 -#: xpack/plugins/license/templates/license/license_detail.html:56 +#: xpack/plugins/license/templates/license/license_detail.html:13 +#: xpack/plugins/license/templates/license/license_detail.html:47 msgid "License has expired" msgstr "许可证已经过期" -#: xpack/plugins/license/templates/license/license_detail.html:22 +#: xpack/plugins/license/templates/license/license_detail.html:15 msgid "The license will at " msgstr "许可证将在 " -#: xpack/plugins/license/templates/license/license_detail.html:22 +#: xpack/plugins/license/templates/license/license_detail.html:15 msgid " expired." msgstr " 过期。" -#: xpack/plugins/license/templates/license/license_detail.html:37 +#: xpack/plugins/license/templates/license/license_detail.html:28 #: xpack/plugins/license/views.py:33 msgid "License detail" msgstr "许可证详情" -#: xpack/plugins/license/templates/license/license_detail.html:51 +#: xpack/plugins/license/templates/license/license_detail.html:42 msgid "No license" msgstr "暂无许可证" -#: xpack/plugins/license/templates/license/license_detail.html:60 +#: xpack/plugins/license/templates/license/license_detail.html:51 msgid "Subscription ID" msgstr "订阅授权ID" -#: xpack/plugins/license/templates/license/license_detail.html:64 +#: xpack/plugins/license/templates/license/license_detail.html:55 msgid "Corporation" msgstr "公司" -#: xpack/plugins/license/templates/license/license_detail.html:68 +#: xpack/plugins/license/templates/license/license_detail.html:59 msgid "Expired" msgstr "过期时间" -#: xpack/plugins/license/templates/license/license_detail.html:73 -#: xpack/plugins/license/templates/license/license_detail.html:77 -#: xpack/plugins/license/templates/license/license_detail.html:81 -#: xpack/plugins/license/templates/license/license_detail.html:85 -msgid "Unlimited" -msgstr "无限制" - -#: xpack/plugins/license/templates/license/license_detail.html:84 -msgid "Concurrent connections" -msgstr "并发连接" - -#: xpack/plugins/license/templates/license/license_detail.html:89 +#: xpack/plugins/license/templates/license/license_detail.html:67 msgid "Edition" msgstr "版本" -#: xpack/plugins/license/templates/license/license_detail.html:115 +#: xpack/plugins/license/templates/license/license_detail.html:93 msgid "Technology consulting" msgstr "技术咨询" -#: xpack/plugins/license/templates/license/license_detail.html:118 +#: xpack/plugins/license/templates/license/license_detail.html:96 msgid "Consult" msgstr "咨询" @@ -6155,12 +6600,12 @@ msgstr "许可证导入成功" msgid "License is invalid" msgstr "无效的许可证" -#: xpack/plugins/orgs/forms.py:24 +#: xpack/plugins/orgs/forms.py:23 msgid "Select auditor" msgstr "选择审计员" -#: xpack/plugins/orgs/forms.py:29 -#: xpack/plugins/orgs/templates/orgs/org_detail.html:76 +#: xpack/plugins/orgs/forms.py:28 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:71 #: xpack/plugins/orgs/templates/orgs/org_list.html:13 msgid "Admin" msgstr "管理员" @@ -6171,12 +6616,12 @@ msgstr "管理员" msgid "Organizations" msgstr "组织管理" -#: xpack/plugins/orgs/templates/orgs/org_detail.html:22 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:17 #: xpack/plugins/orgs/views.py:80 msgid "Org detail" msgstr "组织详情" -#: xpack/plugins/orgs/templates/orgs/org_detail.html:84 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:79 msgid "Add admin" msgstr "添加管理员" @@ -6205,6 +6650,10 @@ msgstr "密码匣子" msgid "Import vault" msgstr "导入密码" +#: xpack/plugins/vault/templates/vault/vault.html:66 +msgid "vault" +msgstr "密码匣子" + #: xpack/plugins/vault/views.py:24 msgid "vault list" msgstr "密码匣子" @@ -6213,14 +6662,290 @@ msgstr "密码匣子" msgid "vault create" msgstr "创建" +#~ msgid "Unlimited" +#~ msgstr "无限制" + +#~ msgid "Concurrent connections" +#~ msgstr "并发连接" + +#~ msgid "Update assets" +#~ msgstr "更新资产" + +#~ msgid "Browser" +#~ msgstr "浏览器" + +#~ msgid "Virtualization tools" +#~ msgstr "虚拟化工具" + +#~ msgid "Import admin user" +#~ msgstr "导入管理用户" + +#~ msgid "Import assets" +#~ msgstr "导入资产" + +#~ msgid "Import system user" +#~ msgstr "导入系统用户" + +#~ msgid "This will delete the selected System Users !!!" +#~ msgstr "删除选择系统用户" + +#~ msgid "System Users Deleted." +#~ msgstr "已被删除" + +#~ msgid "System Users Delete" +#~ msgstr "删除系统用户" + +#~ msgid "System Users Deleting failed." +#~ msgstr "系统用户删除失败" + +#~ msgid "Versions" +#~ msgstr "版本" + +#~ msgid "Import user groups" +#~ msgstr "导入用户组" + +#~ msgid "Import users" +#~ msgstr "导入用户" + +#~ msgid "" +#~ "Jumpserver is an open source desktop system developed using Python and " +#~ "Django that helps Internet businesses with efficient users, assets, " +#~ "permissions, and audit management" +#~ msgstr "" +#~ "Jumpserver是一款使用Python, Django开发的开源跳板机系统, 助力互联网企业高" +#~ "效 用户、资产、权限、审计 管理" + +#~ msgid "" +#~ "We are from all over the world, we have great admiration and worship for " +#~ "the spirit of open source, we have endless pursuit for perfection, " +#~ "neatness and elegance" +#~ msgstr "" +#~ "我们自五湖四海,我们对开源精神无比敬仰和崇拜,我们对完美、整洁、优雅 无止" +#~ "境的追求" + +#~ msgid "" +#~ "We focus on automatic operation and maintenance, and strive to build an " +#~ "easy-to-use, stable, safe and automatic board hopping machine, which is " +#~ "our unremitting pursuit and power" +#~ msgstr "" +#~ "专注自动化运维,努力打造 易用、稳定、安全、自动化 的跳板机, 这是我们的不懈" +#~ "的追求和动力" + +#~ msgid "Always young, always with tears in my eyes. Stay foolish Stay hungry" +#~ msgstr "永远年轻,永远热泪盈眶 stay foolish stay hungry" + +#~ msgid "Password again" +#~ msgstr "再次输入密码" + +#~ msgid "This will delete the selected groups !!!" +#~ msgstr "删除选择组" + +#~ msgid "UserGroups Deleted." +#~ msgstr "用户组删除" + +#~ msgid "UserGroups Delete" +#~ msgstr "用户组删除" + +#~ msgid "UserGroup Deleting failed." +#~ msgstr "用户组删除失败" + +#~ msgid "User Deleted." +#~ msgstr "已被删除" + +#~ msgid "Password not same" +#~ msgstr "密码不一致" + #, fuzzy -#~| msgid "Password should not contain special characters" -#~ msgid "Password has special char" -#~ msgstr "不能包含特殊字符" +#~| msgid "Password update" +#~ msgid "Platform update" +#~ msgstr "密码更新" + +#~ msgid "Search no entry matched in ou {}" +#~ msgstr "在ou:{}中没有匹配条目" + +#~ msgid "Date last active" +#~ msgstr "最后活跃日期" + +#~ msgid "" +#~ "Error: Account invalid (Please make sure the information such as Access " +#~ "key or Secret key is correct)" +#~ msgstr "错误:账户无效 (请确保 Access key 或 Secret key 等信息正确)" + +#~ msgid "Create succeed" +#~ msgstr "创建成功" + +#~ msgid "Delete succeed" +#~ msgstr "删除成功" + +#~ msgid "Tips: If there are multiple hosts, separate them with a comma (,)" +#~ msgstr "提示: 如果有多台主机,请使用逗号 ( , ) 进行分割" + +#~ msgid "OSS: http://{REGION_NAME}.aliyuncs.com" +#~ msgstr "OSS: http://{REGION_NAME}.aliyuncs.com" + +#~ msgid "Example: http://oss-cn-hangzhou.aliyuncs.com" +#~ msgstr "如: http://oss-cn-hangzhou.aliyuncs.com" + +#~ msgid "S3: http://s3.{REGION_NAME}.amazonaws.com" +#~ msgstr "S3: http://s3.{REGION_NAME}.amazonaws.com" + +#~ msgid "S3(China): http://s3.{REGION_NAME}.amazonaws.com.cn" +#~ msgstr "S3(中国): http://s3.{REGION_NAME}.amazonaws.com.cn" + +#~ msgid "Example: http://s3.cn-north-1.amazonaws.com.cn" +#~ msgstr "如: http://s3.cn-north-1.amazonaws.com.cn" + +#~ msgid "Beijing: cn-north-1" +#~ msgstr "北京: cn-north-1" + +#~ msgid "Ningxia: cn-northwest-1" +#~ msgstr "宁夏: cn-northwest-1" + +#~ msgid "More" +#~ msgstr "更多" + +#~ msgid "Submitting" +#~ msgstr "提交中" + +#~ msgid "Endpoint need contain protocol, ex: http" +#~ msgstr "端点需要包含协议,如 http" + +#~ msgid "Delete failed" +#~ msgstr "删除失败" #~ msgid "The connection fails" #~ msgstr "连接失败" +#~ msgid "Assigned ticket" +#~ msgstr "处理人" + +#~ msgid "My ticket" +#~ msgstr "我的工单" + +#~ msgid "User login confirm: {}" +#~ msgstr "用户登录复核: {}" + +#~ msgid "" +#~ "User: {}\n" +#~ "IP: {}\n" +#~ "City: {}\n" +#~ "Date: {}\n" +#~ msgstr "" +#~ "用户: {}\n" +#~ "IP: {}\n" +#~ "城市: {}\n" +#~ "日期: {}\n" + +#~ msgid "this order" +#~ msgstr "这个工单" + +#~ msgid "Approve selected" +#~ msgstr "同意所选" + +#~ msgid "" +#~ "\n" +#~ "
    \n" +#~ "

    Your has a new ticket

    \n" +#~ "
    \n" +#~ " Title: {ticket.title}\n" +#~ "
    \n" +#~ " User: {user}\n" +#~ "
    \n" +#~ " Assignees: {ticket.assignees_display}\n" +#~ "
    \n" +#~ " City: {ticket.city}\n" +#~ "
    \n" +#~ " IP: {ticket.ip}\n" +#~ "
    \n" +#~ " click here to review \n" +#~ "
    \n" +#~ "
    \n" +#~ " " +#~ msgstr "" +#~ "\n" +#~ "
    \n" +#~ "

    您有一个新工单

    \n" +#~ "
    \n" +#~ " 标题: {ticket.title}\n" +#~ "
    \n" +#~ " 用户: {user}\n" +#~ "
    \n" +#~ " 待处理人: {ticket.assignees_display}\n" +#~ "
    \n" +#~ " 城市: {ticket.city}\n" +#~ "
    \n" +#~ " IP: {ticket.ip}\n" +#~ "
    \n" +#~ " 点我查看 \n" +#~ "
    \n" +#~ "
    \n" +#~ " " + +#~ msgid "Login confirm ticket list" +#~ msgstr "登录复核工单列表" + +#~ msgid "Login confirm ticket detail" +#~ msgstr "登录复核工单详情" + +#, fuzzy +#~| msgid "Login" +#~ msgid "Login IP" +#~ msgstr "登录" + +#~ msgid "succeed: {} failed: {} total: {}" +#~ msgstr "成功:{} 失败:{} 总数:{}" + +#~ msgid "The user source is not LDAP" +#~ msgstr "用户来源不是LDAP" + +#~ msgid "selected" +#~ msgstr "所选" + +#~ msgid "not found" +#~ msgstr "没有发现" + +#~ msgid "Log in frequently and try again later" +#~ msgstr "登录频繁, 稍后重试" + +#~ msgid "Please carry seed value and conduct MFA secondary certification" +#~ msgstr "请携带seed值, 进行MFA二次认证" + +#~ msgid "Please verify the user name and password first" +#~ msgstr "请先进行用户名和密码验证" + +#~ msgid "MFA certification failed" +#~ msgstr "MFA认证失败" + +#~ msgid "Accepted" +#~ msgstr "已接受" + +#~ msgid "New order" +#~ msgstr "新工单" + +#~ msgid "Orders" +#~ msgstr "工单管理" + +#~ msgid "" +#~ "The username or password you entered is incorrect, please enter it again." +#~ msgstr "您输入的用户名或密码不正确,请重新输入。" + +#~ msgid "" +#~ "You can also try {times_try} times (The account will be temporarily " +#~ "locked for {block_time} minutes)" +#~ msgstr "您还可以尝试 {times_try} 次(账号将被临时锁定 {block_time} 分钟)" + +#~ msgid "No order found or order expired" +#~ msgstr "没有找到工单,或者已过期" + +#~ msgid "Order was rejected by {}" +#~ msgstr "工单被拒绝 {}" + +#~ msgid "login_confirm_setting" +#~ msgstr "登录复核设置" + +#~ msgid "The user password has expired" +#~ msgstr "用户密码已过期" + #~ msgid "Recipient" #~ msgstr "收件人" @@ -6254,9 +6979,6 @@ msgstr "创建" #~ msgid "already exists" #~ msgstr "已经存在" -#~ msgid "Invalid file." -#~ msgstr "文件不合法" - #~ msgid "Refresh all node assets amount" #~ msgstr "刷新所有节点资产数量" @@ -6360,9 +7082,6 @@ msgstr "创建" #~ msgid "Valid" #~ msgstr "账户状态" -#~ msgid "Error: Account invalid" -#~ msgstr "错误: 账户无效" - #~ msgid "Asset has been disabled, skip: {}" #~ msgstr "资产被禁用,跳过:{}" @@ -6387,9 +7106,6 @@ msgstr "创建" #~ msgid "Start" #~ msgstr "开始" -#~ msgid "User login settings" -#~ msgstr "用户登录设置" - #~ msgid "Bit" #~ msgstr " 位" diff --git a/jumpserver/jumpserver/apps/locale/zh/LC_MESSAGES/djangojs.mo b/jumpserver/jumpserver/apps/locale/zh/LC_MESSAGES/djangojs.mo index 4bd390695a945e5f13744670c777354e7d85cc70..24e68caf523648ed333a85c565401a6fbac6a37e 100644 Binary files a/jumpserver/jumpserver/apps/locale/zh/LC_MESSAGES/djangojs.mo and b/jumpserver/jumpserver/apps/locale/zh/LC_MESSAGES/djangojs.mo differ diff --git a/jumpserver/jumpserver/apps/ops/ansible/runner.py b/jumpserver/jumpserver/apps/ops/ansible/runner.py index 5f25ee2c49d1912d96a29ed7732fedfaa980e4ef..b6c6b8ee14c5a1b457b455d055c174c7bfe6698a 100644 --- a/jumpserver/jumpserver/apps/ops/ansible/runner.py +++ b/jumpserver/jumpserver/apps/ops/ansible/runner.py @@ -228,7 +228,7 @@ class AdHocRunner: class CommandRunner(AdHocRunner): results_callback_class = CommandResultCallback - modules_choices = ('shell', 'raw', 'command', 'script') + modules_choices = ('shell', 'raw', 'command', 'script', 'win_shell') def execute(self, cmd, pattern, module='shell'): if module and module not in self.modules_choices: diff --git a/jumpserver/jumpserver/apps/ops/api/adhoc.py b/jumpserver/jumpserver/apps/ops/api/adhoc.py index 1bf2b4901acc01ac8e8741efe5e0704dcf7da7fb..38c59d70106bbe84d1b33c9d6e1269c788460130 100644 --- a/jumpserver/jumpserver/apps/ops/api/adhoc.py +++ b/jumpserver/jumpserver/apps/ops/api/adhoc.py @@ -4,6 +4,7 @@ from django.shortcuts import get_object_or_404 from rest_framework import viewsets, generics from rest_framework.views import Response +from django.db.models import Count, Q from common.permissions import IsOrgAdmin from common.serializers import CeleryTaskSerializer @@ -31,6 +32,7 @@ class TaskViewSet(viewsets.ModelViewSet): queryset = queryset.filter(created_by=current_org.id) else: queryset = queryset.filter(created_by='') + queryset = queryset.select_related('latest_history') return queryset diff --git a/jumpserver/jumpserver/apps/ops/api/celery.py b/jumpserver/jumpserver/apps/ops/api/celery.py index 6be58770f828e0d9ca4d3b55ca4cf5db38c9b1cc..814835465e2b547b811facab734f951007357f89 100644 --- a/jumpserver/jumpserver/apps/ops/api/celery.py +++ b/jumpserver/jumpserver/apps/ops/api/celery.py @@ -5,17 +5,20 @@ import os import re from django.utils.translation import ugettext as _ +from rest_framework import viewsets from celery.result import AsyncResult from rest_framework import generics +from django_celery_beat.models import PeriodicTask -from common.permissions import IsValidUser +from common.permissions import IsValidUser, IsSuperUser from common.api import LogTailApi from ..models import CeleryTask -from ..serializers import CeleryResultSerializer +from ..serializers import CeleryResultSerializer, CeleryPeriodTaskSerializer from ..celery.utils import get_celery_task_log_path +from common.mixins.api import CommonApiMixin -__all__ = ['CeleryTaskLogApi', 'CeleryResultApi'] +__all__ = ['CeleryTaskLogApi', 'CeleryResultApi', 'CeleryPeriodTaskViewSet'] class CeleryTaskLogApi(LogTailApi): @@ -62,3 +65,14 @@ class CeleryResultApi(generics.RetrieveAPIView): pk = self.kwargs.get('pk') return AsyncResult(pk) + +class CeleryPeriodTaskViewSet(CommonApiMixin, viewsets.ModelViewSet): + queryset = PeriodicTask.objects.all() + serializer_class = CeleryPeriodTaskSerializer + permission_classes = (IsSuperUser,) + http_method_names = ('get', 'head', 'options', 'patch') + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.exclude(description='') + return queryset diff --git a/jumpserver/jumpserver/apps/ops/celery/decorator.py b/jumpserver/jumpserver/apps/ops/celery/decorator.py index c2052f832b41e311e0a7b8d1123678791c49e933..971d5b86348116fda3d02e070d59a8ef3e900df4 100644 --- a/jumpserver/jumpserver/apps/ops/celery/decorator.py +++ b/jumpserver/jumpserver/apps/ops/celery/decorator.py @@ -9,51 +9,40 @@ _after_app_shutdown_clean_periodic_tasks = [] def add_register_period_task(task): _need_registered_period_tasks.append(task) - # key = "__REGISTER_PERIODIC_TASKS" - # value = cache.get(key, []) - # value.append(name) - # cache.set(key, value) def get_register_period_tasks(): - # key = "__REGISTER_PERIODIC_TASKS" - # return cache.get(key, []) return _need_registered_period_tasks def add_after_app_shutdown_clean_task(name): - # key = "__AFTER_APP_SHUTDOWN_CLEAN_TASKS" - # value = cache.get(key, []) - # value.append(name) - # cache.set(key, value) _after_app_shutdown_clean_periodic_tasks.append(name) def get_after_app_shutdown_clean_tasks(): - # key = "__AFTER_APP_SHUTDOWN_CLEAN_TASKS" - # return cache.get(key, []) return _after_app_shutdown_clean_periodic_tasks def add_after_app_ready_task(name): - # key = "__AFTER_APP_READY_RUN_TASKS" - # value = cache.get(key, []) - # value.append(name) - # cache.set(key, value) _after_app_ready_start_tasks.append(name) def get_after_app_ready_tasks(): - # key = "__AFTER_APP_READY_RUN_TASKS" - # return cache.get(key, []) return _after_app_ready_start_tasks -def register_as_period_task(crontab=None, interval=None): +def register_as_period_task( + crontab=None, interval=None, name=None, + args=(), kwargs=None, + description=''): """ Warning: Task must be have not any args and kwargs :param crontab: "* * * * *" :param interval: 60*60*60 + :param args: () + :param kwargs: {} + :param description: " + :param name: "" :return: """ if crontab is None and interval is None: @@ -65,14 +54,17 @@ def register_as_period_task(crontab=None, interval=None): # Because when this decorator run, the task was not created, # So we can't use func.name - name = '{func.__module__}.{func.__name__}'.format(func=func) + task = '{func.__module__}.{func.__name__}'.format(func=func) + _name = name if name else task add_register_period_task({ - name: { - 'task': name, + _name: { + 'task': task, 'interval': interval, 'crontab': crontab, - 'args': (), + 'args': args, + 'kwargs': kwargs if kwargs else {}, 'enabled': True, + 'description': description } }) diff --git a/jumpserver/jumpserver/apps/ops/celery/signal_handler.py b/jumpserver/jumpserver/apps/ops/celery/signal_handler.py index 0f6312a24ec07b8e968424652d7a274c8c7a528a..5d5fc4227dd49c942c2f3ee47a52e7b602ba76e2 100644 --- a/jumpserver/jumpserver/apps/ops/celery/signal_handler.py +++ b/jumpserver/jumpserver/apps/ops/celery/signal_handler.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # import logging +from django.dispatch import receiver from django.core.cache import cache from celery import subtask @@ -11,6 +12,7 @@ from kombu.utils.encoding import safe_str from django_celery_beat.models import PeriodicTask from common.utils import get_logger +from common.signals import django_ready from .decorator import get_after_app_ready_tasks, get_after_app_shutdown_clean_tasks from .logger import CeleryTaskFileHandler diff --git a/jumpserver/jumpserver/apps/ops/celery/utils.py b/jumpserver/jumpserver/apps/ops/celery/utils.py index f9a80e7941322a751c6f566f9e6e4e7571113f11..55ce4a9d193c94eafa2a6067413aa7bbd9b8fb99 100644 --- a/jumpserver/jumpserver/apps/ops/celery/utils.py +++ b/jumpserver/jumpserver/apps/ops/celery/utils.py @@ -10,6 +10,10 @@ from django_celery_beat.models import ( PeriodicTask, IntervalSchedule, CrontabSchedule, PeriodicTasks ) +from common.utils import get_logger + +logger = get_logger(__name__) + def create_or_update_celery_periodic_tasks(tasks): """ @@ -21,6 +25,7 @@ def create_or_update_celery_periodic_tasks(tasks): 'args': (16, 16), 'kwargs': {}, 'enabled': False, + 'description': '' }, } :return: @@ -35,34 +40,30 @@ def create_or_update_celery_periodic_tasks(tasks): return None if isinstance(detail.get("interval"), int): - intervals = IntervalSchedule.objects.filter( - every=detail["interval"], period=IntervalSchedule.SECONDS + kwargs = dict( + every=detail['interval'], + period=IntervalSchedule.SECONDS, ) - if intervals: - interval = intervals[0] - else: - interval = IntervalSchedule.objects.create( - every=detail['interval'], - period=IntervalSchedule.SECONDS, - ) + # 不能使用 get_or_create,因为可能会有多个 + interval = IntervalSchedule.objects.filter(**kwargs).first() + if interval is None: + interval = IntervalSchedule.objects.create(**kwargs) elif isinstance(detail.get("crontab"), str): try: minute, hour, day, month, week = detail["crontab"].split() except ValueError: - raise SyntaxError("crontab is not valid") + logger.error("crontab is not valid") + return kwargs = dict( minute=minute, hour=hour, day_of_week=week, day_of_month=day, month_of_year=month, timezone=get_current_timezone() ) - contabs = CrontabSchedule.objects.filter( - **kwargs - ) - if contabs: - crontab = contabs[0] - else: + crontab = CrontabSchedule.objects.filter(**kwargs).first() + if crontab is None: crontab = CrontabSchedule.objects.create(**kwargs) else: - raise SyntaxError("Schedule is not valid") + logger.error("Schedule is not valid") + return defaults = dict( interval=interval, @@ -71,9 +72,8 @@ def create_or_update_celery_periodic_tasks(tasks): task=detail['task'], args=json.dumps(detail.get('args', [])), kwargs=json.dumps(detail.get('kwargs', {})), - enabled=detail.get('enabled', True), + description=detail.get('description') or '' ) - task = PeriodicTask.objects.update_or_create( defaults=defaults, name=name, ) @@ -99,4 +99,3 @@ def get_celery_task_log_path(task_id): path = os.path.join(settings.CELERY_LOG_DIR, rel_path) os.makedirs(os.path.dirname(path), exist_ok=True) return path - diff --git a/jumpserver/jumpserver/apps/ops/migrations/0009_auto_20191217_1713.py b/jumpserver/jumpserver/apps/ops/migrations/0009_auto_20191217_1713.py new file mode 100644 index 0000000000000000000000000000000000000000..75ba632d883fd2a7ff38279f144ee6fc284d92ae --- /dev/null +++ b/jumpserver/jumpserver/apps/ops/migrations/0009_auto_20191217_1713.py @@ -0,0 +1,72 @@ +# Generated by Django 2.2.7 on 2019-12-17 09:13 + +from django.db import migrations, models +import django.db.models.deletion +from django.core.exceptions import ObjectDoesNotExist + + +def migrate_task_data(apps, schema_editor): + task_model = apps.get_model("ops", "Task") + db_alias = schema_editor.connection.alias + tasks = task_model.objects.using(db_alias).all() + for task in tasks: + try: + latest_history = task.history.latest() + except ObjectDoesNotExist: + latest_history = None + try: + latest_adhoc = task.adhoc.latest() + except ObjectDoesNotExist: + latest_adhoc = None + if latest_history and latest_history.adhoc: + latest_history.hosts_amount = latest_history.adhoc.hosts.count() + latest_history.save() + total_run_amount = task.history.all().count() + success_run_amount = task.history.filter(is_success=True).count() + task.latest_history = latest_history + task.latest_adhoc = latest_adhoc + task.total_run_amount = total_run_amount + task.success_run_amount = success_run_amount + task.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0008_auto_20190919_2100'), + ] + + operations = [ + migrations.AddField( + model_name='task', + name='latest_adhoc', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='task_latest', to='ops.AdHoc'), + ), + migrations.AddField( + model_name='task', + name='latest_history', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='task_latest', to='ops.AdHocRunHistory'), + ), + migrations.AddField( + model_name='task', + name='success_run_amount', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='task', + name='total_run_amount', + field=models.IntegerField(default=0), + ), + migrations.AddField( + model_name='adhocrunhistory', + name='hosts_amount', + field=models.IntegerField(default=0, verbose_name='Host amount'), + ), + migrations.AddField( + model_name='adhocrunhistory', + name='task_display', + field=models.CharField(blank=True, default='', max_length=128, + verbose_name='Task display'), + ), + migrations.RunPython(migrate_task_data), + ] diff --git a/jumpserver/jumpserver/apps/ops/migrations/0010_auto_20191217_1758.py b/jumpserver/jumpserver/apps/ops/migrations/0010_auto_20191217_1758.py new file mode 100644 index 0000000000000000000000000000000000000000..a9a1555c0dc3c78836d0d80e9c45816655ea25b4 --- /dev/null +++ b/jumpserver/jumpserver/apps/ops/migrations/0010_auto_20191217_1758.py @@ -0,0 +1,68 @@ +# Generated by Django 2.2.7 on 2019-12-17 09:58 + +import common.fields.model +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0009_auto_20191217_1713'), + ] + + operations = [ + migrations.RemoveField( + model_name='adhoc', + name='_hosts', + ), + migrations.AlterField( + model_name='adhoc', + name='_become', + field=common.fields.model.EncryptJsonDictCharField(blank=True, null=True, default='', max_length=1024, verbose_name='Become'), + ), + migrations.AlterField( + model_name='adhoc', + name='_options', + field=common.fields.model.JsonDictCharField(default='', max_length=1024, verbose_name='Options'), + ), + migrations.AlterField( + model_name='adhoc', + name='_tasks', + field=common.fields.model.JsonListTextField(verbose_name='Tasks'), + ), + migrations.RenameField( + model_name='adhoc', + old_name='_become', + new_name='become', + ), + migrations.RenameField( + model_name='adhoc', + old_name='_options', + new_name='options', + ), + migrations.RenameField( + model_name='adhoc', + old_name='_tasks', + new_name='tasks', + ), + migrations.AlterField( + model_name='adhocrunhistory', + name='_result', + field=common.fields.model.JsonDictTextField(blank=True, null=True, verbose_name='Adhoc raw result'), + ), + migrations.AlterField( + model_name='adhocrunhistory', + name='_summary', + field=common.fields.model.JsonDictTextField(blank=True, null=True, verbose_name='Adhoc result summary'), + ), + migrations.RenameField( + model_name='adhocrunhistory', + old_name='_result', + new_name='result', + ), + migrations.RenameField( + model_name='adhocrunhistory', + old_name='_summary', + new_name='summary', + ), + ] diff --git a/jumpserver/jumpserver/apps/ops/models/adhoc.py b/jumpserver/jumpserver/apps/ops/models/adhoc.py index b9c1c4a74621be859f384f810a70c9a4ddfce2b0..73dfd4339c7efc06cb3a2099e640f97da3831f65 100644 --- a/jumpserver/jumpserver/apps/ops/models/adhoc.py +++ b/jumpserver/jumpserver/apps/ops/models/adhoc.py @@ -1,6 +1,5 @@ # ~*~ coding: utf-8 ~*~ -import json import uuid import os import time @@ -13,11 +12,16 @@ from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from django_celery_beat.models import PeriodicTask -from common.utils import get_signer, get_logger, lazyproperty -from orgs.utils import set_to_root_org -from ..celery.utils import delete_celery_periodic_task, \ - create_or_update_celery_periodic_tasks, \ +from common.utils import get_logger, lazyproperty +from common.fields.model import ( + JsonListTextField, JsonDictCharField, EncryptJsonDictCharField, + JsonDictTextField, +) +from orgs.utils import set_to_root_org, get_current_org, set_current_org +from ..celery.utils import ( + delete_celery_periodic_task, create_or_update_celery_periodic_tasks, disable_celery_periodic_task +) from ..ansible import AdHocRunner, AnsibleError from ..inventory import JMSInventory @@ -25,7 +29,6 @@ __all__ = ["Task", "AdHoc", "AdHocRunHistory"] logger = get_logger(__file__) -signer = get_signer() class Task(models.Model): @@ -44,14 +47,17 @@ class Task(models.Model): created_by = models.CharField(max_length=128, blank=True, default='') date_created = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name=_("Date created")) date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated")) - __latest_adhoc = None + latest_adhoc = models.ForeignKey('ops.AdHoc', on_delete=models.SET_NULL, null=True, related_name='task_latest') + latest_history = models.ForeignKey('ops.AdHocRunHistory', on_delete=models.SET_NULL, null=True, related_name='task_latest') + total_run_amount = models.IntegerField(default=0) + success_run_amount = models.IntegerField(default=0) _ignore_auto_created_by = True @property def short_id(self): return str(self.id).split('-')[-1] - @property + @lazyproperty def versions(self): return self.adhoc.all().count() @@ -78,73 +84,67 @@ class Task(models.Model): @property def assets_amount(self): - return self.latest_adhoc.hosts.count() - - @lazyproperty - def latest_adhoc(self): - return self.get_latest_adhoc() - - @lazyproperty - def latest_history(self): - try: - return self.history.all().latest() - except AdHocRunHistory.DoesNotExist: - return None + if self.latest_history: + return self.latest_history.hosts_amount + return 0 def get_latest_adhoc(self): + if self.latest_adhoc: + return self.latest_adhoc try: - return self.adhoc.all().latest() + adhoc = self.adhoc.all().latest() + self.latest_adhoc = adhoc + self.save() + return adhoc except AdHoc.DoesNotExist: return None @property def history_summary(self): - history = self.get_run_history() - total = len(history) - success = len([history for history in history if history.is_success]) - failed = len([history for history in history if not history.is_success]) + total = self.total_run_amount + success = self.success_run_amount + failed = total - success return {'total': total, 'success': success, 'failed': failed} def get_run_history(self): return self.history.all() - def run(self, record=True): - set_to_root_org() - if self.latest_adhoc: - return self.latest_adhoc.run(record=record) + def run(self): + latest_adhoc = self.get_latest_adhoc() + if latest_adhoc: + return latest_adhoc.run() else: return {'error': 'No adhoc'} - def save(self, force_insert=False, force_update=False, using=None, - update_fields=None): + def register_as_period_task(self): from ..tasks import run_ansible_task - super().save( - force_insert=force_insert, force_update=force_update, - using=using, update_fields=update_fields, - ) + interval = None + crontab = None + + if self.interval: + interval = self.interval + elif self.crontab: + crontab = self.crontab + + tasks = { + self.__str__(): { + "task": run_ansible_task.name, + "interval": interval, + "crontab": crontab, + "args": (str(self.id),), + "kwargs": {"callback": self.callback}, + "enabled": True, + } + } + create_or_update_celery_periodic_tasks(tasks) + def save(self, **kwargs): + instance = super().save(**kwargs) if self.is_periodic: - interval = None - crontab = None - - if self.interval: - interval = self.interval - elif self.crontab: - crontab = self.crontab - - tasks = { - self.__str__(): { - "task": run_ansible_task.name, - "interval": interval, - "crontab": crontab, - "args": (str(self.id),), - "kwargs": {"callback": self.callback}, - "enabled": True, - } - } - create_or_update_celery_periodic_tasks(tasks) + self.register_as_period_task() else: disable_celery_periodic_task(self.__str__()) + return instance def delete(self, using=None, keep_parents=False): super().delete(using=using, keep_parents=keep_parents) @@ -153,7 +153,7 @@ class Task(models.Model): @property def schedule(self): try: - return PeriodicTask.objects.get(name=self.name) + return PeriodicTask.objects.get(name=str(self)) except PeriodicTask.DoesNotExist: return None @@ -172,7 +172,6 @@ class AdHoc(models.Model): task: A task reference _tasks: [{'name': 'task_name', 'action': {'module': '', 'args': ''}, 'other..': ''}, ] _options: ansible options, more see ops.ansible.runner.Options - _hosts: ["hostname1", "hostname2"], hostname must be unique key of cmdb run_as_admin: if true, then need get every host admin user run it, because every host may be have different admin user, so we choise host level run_as: username(Add the uniform AssetUserManager and change it to username) _become: May be using become [sudo, su] options. {method: "sudo", user: "user", pass: "pass"] @@ -180,31 +179,16 @@ class AdHoc(models.Model): """ id = models.UUIDField(default=uuid.uuid4, primary_key=True) task = models.ForeignKey(Task, related_name='adhoc', on_delete=models.CASCADE) - _tasks = models.TextField(verbose_name=_('Tasks')) + tasks = JsonListTextField(verbose_name=_('Tasks')) pattern = models.CharField(max_length=64, default='{}', verbose_name=_('Pattern')) - _options = models.CharField(max_length=1024, default='', verbose_name=_('Options')) - _hosts = models.TextField(blank=True, verbose_name=_('Hosts')) # ['hostname1', 'hostname2'] + options = JsonDictCharField(max_length=1024, default='', verbose_name=_('Options')) hosts = models.ManyToManyField('assets.Asset', verbose_name=_("Host")) run_as_admin = models.BooleanField(default=False, verbose_name=_('Run as admin')) run_as = models.CharField(max_length=64, default='', blank=True, null=True, verbose_name=_('Username')) - _become = models.CharField(max_length=1024, default='', blank=True, verbose_name=_("Become")) + become = EncryptJsonDictCharField(max_length=1024, default='', null=True, blank=True, verbose_name=_("Become")) created_by = models.CharField(max_length=64, default='', blank=True, null=True, verbose_name=_('Create by')) date_created = models.DateTimeField(auto_now_add=True, db_index=True) - @property - def tasks(self): - try: - return json.loads(self._tasks) - except: - return [] - - @tasks.setter - def tasks(self, item): - if item and isinstance(item, list): - self._tasks = json.dumps(item) - else: - raise SyntaxError('Tasks should be a list: {}'.format(item)) - @property def inventory(self): if self.become: @@ -223,92 +207,22 @@ class AdHoc(models.Model): return inventory @property - def become(self): - if self._become: - return json.loads(signer.unsign(self._become)) - else: - return {} - - def run(self, record=True): - set_to_root_org() - if record: - return self._run_and_record() - else: - return self._run_only() + def become_display(self): + if self.become: + return self.become.get("user", "") + return "" - def _run_and_record(self): + def run(self): try: hid = current_task.request.id except AttributeError: hid = str(uuid.uuid4()) - history = AdHocRunHistory(id=hid, adhoc=self, task=self.task) + history = AdHocRunHistory( + id=hid, adhoc=self, task=self.task, + task_display=str(self.task) + ) history.save() - time_start = time.time() - date_start = timezone.now() - is_success = False - - try: - date_start_s = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') - print(_("{} Start task: {}").format(date_start_s, self.task.name)) - raw, summary = self._run_only() - is_success = summary.get('success', False) - return raw, summary - except Exception as e: - logger.error(e, exc_info=True) - summary = {} - raw = {"dark": {"all": str(e)}, "contacted": []} - return raw, summary - finally: - date_end = timezone.now() - date_end_s = date_end.strftime('%Y-%m-%d %H:%M:%S') - print(_("{} Task finish").format(date_end_s)) - print('.\n\n.') - AdHocRunHistory.objects.filter(id=history.id).update( - date_start=date_start, - is_finished=True, - is_success=is_success, - date_finished=timezone.now(), - timedelta=time.time() - time_start - ) - - def _run_only(self): - Task.objects.filter(id=self.task.id).update(date_updated=timezone.now()) - runner = AdHocRunner(self.inventory, options=self.options) - try: - result = runner.run( - self.tasks, - self.pattern, - self.task.name, - ) - return result.results_raw, result.results_summary - except AnsibleError as e: - logger.warn("Failed run adhoc {}, {}".format(self.task.name, e)) - pass - - @become.setter - def become(self, item): - """ - :param item: { - method: "sudo", - user: "user", - pass: "pass", - } - :return: - """ - # self._become = signer.sign(json.dumps(item)).decode('utf-8') - self._become = signer.sign(json.dumps(item)) - - @property - def options(self): - if self._options: - _options = json.loads(self._options) - if isinstance(_options, dict): - return _options - return {} - - @options.setter - def options(self, item): - self._options = json.dumps(item) + return history.start() @property def short_id(self): @@ -321,10 +235,11 @@ class AdHoc(models.Model): except AdHocRunHistory.DoesNotExist: return None - def save(self, force_insert=False, force_update=False, using=None, - update_fields=None): - super().save(force_insert=force_insert, force_update=force_update, - using=using, update_fields=update_fields) + def save(self, **kwargs): + instance = super().save(**kwargs) + self.task.latest_adhoc = instance + self.task.save() + return instance def __str__(self): return "{} of {}".format(self.task.name, self.short_id) @@ -352,19 +267,25 @@ class AdHocRunHistory(models.Model): """ id = models.UUIDField(default=uuid.uuid4, primary_key=True) task = models.ForeignKey(Task, related_name='history', on_delete=models.SET_NULL, null=True) + task_display = models.CharField(max_length=128, blank=True, default='', verbose_name=_("Task display")) + hosts_amount = models.IntegerField(default=0, verbose_name=_("Host amount")) adhoc = models.ForeignKey(AdHoc, related_name='history', on_delete=models.SET_NULL, null=True) date_start = models.DateTimeField(auto_now_add=True, verbose_name=_('Start time')) date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('End time')) timedelta = models.FloatField(default=0.0, verbose_name=_('Time'), null=True) is_finished = models.BooleanField(default=False, verbose_name=_('Is finished')) is_success = models.BooleanField(default=False, verbose_name=_('Is success')) - _result = models.TextField(blank=True, null=True, verbose_name=_('Adhoc raw result')) - _summary = models.TextField(blank=True, null=True, verbose_name=_('Adhoc result summary')) + result = JsonDictTextField(blank=True, null=True, verbose_name=_('Adhoc raw result')) + summary = JsonDictTextField(blank=True, null=True, verbose_name=_('Adhoc result summary')) @property def short_id(self): return str(self.id).split('-')[-1] + @property + def adhoc_short_id(self): + return str(self.adhoc_id).split('-')[-1] + @property def log_path(self): dt = datetime.datetime.now().strftime('%Y-%m-%d') @@ -373,27 +294,58 @@ class AdHocRunHistory(models.Model): os.makedirs(log_dir) return os.path.join(log_dir, str(self.id) + '.log') - @property - def result(self): - if self._result: - return json.loads(self._result) - else: - return {} - - @result.setter - def result(self, item): - self._result = json.dumps(item) + def start_runner(self): + runner = AdHocRunner(self.adhoc.inventory, options=self.adhoc.options) + try: + result = runner.run( + self.adhoc.tasks, + self.adhoc.pattern, + self.task.name, + ) + return result.results_raw, result.results_summary + except AnsibleError as e: + logger.warn("Failed run adhoc {}, {}".format(self.task.name, e)) + return {}, {} - @property - def summary(self): - if self._summary: - return json.loads(self._summary) - else: - return {"ok": {}, "dark": {}} + def start(self): + self.task.latest_history = self + self.task.save() + current_org = get_current_org() + set_to_root_org() + time_start = time.time() + date_start = timezone.now() + is_success = False + summary = {} + raw = '' - @summary.setter - def summary(self, item): - self._summary = json.dumps(item) + try: + date_start_s = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + print(_("{} Start task: {}").format(date_start_s, self.task.name)) + raw, summary = self.start_runner() + is_success = summary.get('success', False) + except Exception as e: + logger.error(e, exc_info=True) + raw = {"dark": {"all": str(e)}, "contacted": []} + finally: + date_end = timezone.now() + date_end_s = date_end.strftime('%Y-%m-%d %H:%M:%S') + print(_("{} Task finish").format(date_end_s)) + print('.\n\n.') + task = Task.objects.get(id=self.task_id) + task.total_run_amount = models.F('total_run_amount') + 1 + if is_success: + task.success_run_amount = models.F('success_run_amount') + 1 + task.save() + AdHocRunHistory.objects.filter(id=self.id).update( + date_start=date_start, + is_finished=True, + is_success=is_success, + date_finished=timezone.now(), + timedelta=time.time() - time_start, + summary=summary + ) + set_current_org(current_org) + return raw, summary @property def success_hosts(self): diff --git a/jumpserver/jumpserver/apps/ops/models/command.py b/jumpserver/jumpserver/apps/ops/models/command.py index dfa1dbe4dcc05352252aa43e386f161b9041baa0..9ce44e20f38d8ae2925c75e139a459d38cca8dcc 100644 --- a/jumpserver/jumpserver/apps/ops/models/command.py +++ b/jumpserver/jumpserver/apps/ops/models/command.py @@ -3,6 +3,7 @@ import uuid import json +from celery.exceptions import SoftTimeLimitExceeded from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext @@ -62,8 +63,16 @@ class CommandExecution(models.Model): if ok: runner = CommandRunner(self.inventory) try: - result = runner.execute(self.command, 'all') + host = self.hosts.first() + if host.is_windows(): + shell = 'win_shell' + else: + shell = 'shell' + result = runner.execute(self.command, 'all', module=shell) self.result = result.results_command + except SoftTimeLimitExceeded as e: + print("Run timeout than 60s") + self.result = {"error": str(e)} except Exception as e: print("Error occur: {}".format(e)) self.result = {"error": str(e)} diff --git a/jumpserver/jumpserver/apps/ops/serializers/__init__.py b/jumpserver/jumpserver/apps/ops/serializers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..166827b4b71c15ca83d7037c25bcc2da475dd257 --- /dev/null +++ b/jumpserver/jumpserver/apps/ops/serializers/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# + +from .celery import * +from .adhoc import * diff --git a/jumpserver/jumpserver/apps/ops/serializers.py b/jumpserver/jumpserver/apps/ops/serializers/adhoc.py similarity index 61% rename from jumpserver/jumpserver/apps/ops/serializers.py rename to jumpserver/jumpserver/apps/ops/serializers/adhoc.py index c3df9d1ef8e60e1906013b640ae5fc0bd4761f9b..6a34646543a8dadde22ab64f6d3140c11e1e7a46 100644 --- a/jumpserver/jumpserver/apps/ops/serializers.py +++ b/jumpserver/jumpserver/apps/ops/serializers/adhoc.py @@ -3,72 +3,77 @@ from __future__ import unicode_literals from rest_framework import serializers from django.shortcuts import reverse -from .models import Task, AdHoc, AdHocRunHistory, CommandExecution - - -class CeleryResultSerializer(serializers.Serializer): - id = serializers.UUIDField() - result = serializers.JSONField() - state = serializers.CharField(max_length=16) - - -class CeleryTaskSerializer(serializers.Serializer): - pass - - -class TaskSerializer(serializers.ModelSerializer): - class Meta: - model = Task - fields = [ - 'id', 'name', 'interval', 'crontab', 'is_periodic', - 'is_deleted', 'comment', 'created_by', 'date_created', - 'versions', 'is_success', 'timedelta', 'assets_amount', - 'date_updated', 'history_summary', - ] - - -class AdHocSerializer(serializers.ModelSerializer): - class Meta: - model = AdHoc - exclude = ('_tasks', '_options', '_hosts', '_become') - - def get_field_names(self, declared_fields, info): - fields = super().get_field_names(declared_fields, info) - fields.extend(['tasks', 'options', 'hosts', 'become', 'short_id']) - return fields +from ..models import Task, AdHoc, AdHocRunHistory, CommandExecution class AdHocRunHistorySerializer(serializers.ModelSerializer): - task = serializers.SerializerMethodField() - adhoc_short_id = serializers.SerializerMethodField() stat = serializers.SerializerMethodField() class Meta: model = AdHocRunHistory - exclude = ('_result', '_summary') - - @staticmethod - def get_adhoc_short_id(obj): - return obj.adhoc.short_id + fields = '__all__' @staticmethod def get_task(obj): - return obj.adhoc.task.id + return obj.task.id @staticmethod def get_stat(obj): return { - "total": obj.adhoc.hosts.count(), + "total": obj.hosts_amount, "success": len(obj.summary.get("contacted", [])), "failed": len(obj.summary.get("dark", [])), } def get_field_names(self, declared_fields, info): fields = super().get_field_names(declared_fields, info) - fields.extend(['summary', 'short_id']) + fields.extend(['short_id', 'adhoc_short_id']) return fields +class AdHocRunHistoryExcludeResultSerializer(AdHocRunHistorySerializer): + def get_field_names(self, declared_fields, info): + fields = super().get_field_names(declared_fields, info) + fields = [i for i in fields if i not in ['result', 'summary']] + return fields + + +class TaskSerializer(serializers.ModelSerializer): + latest_history = AdHocRunHistoryExcludeResultSerializer(read_only=True) + + class Meta: + model = Task + fields = [ + 'id', 'name', 'interval', 'crontab', 'is_periodic', + 'is_deleted', 'comment', 'created_by', 'date_created', + 'date_updated', 'latest_history', + ] + read_only_fields = [ + 'is_deleted', 'created_by', 'date_created', 'date_updated', + 'latest_adhoc', 'latest_history', 'total_run_amount', + 'success_run_amount', + ] + + +class AdHocSerializer(serializers.ModelSerializer): + become_display = serializers.ReadOnlyField() + + class Meta: + model = AdHoc + fields = [ + "id", "task", 'tasks', "pattern", "options", + "hosts", "run_as_admin", "run_as", "become", + "created_by", "date_created", "short_id", + "become_display", + ] + read_only_fields = [ + 'created_by', 'date_created' + ] + extra_kwargs = { + "become": {'write_only': True} + } + + class CommandExecutionSerializer(serializers.ModelSerializer): result = serializers.JSONField(read_only=True) log_url = serializers.SerializerMethodField() @@ -87,3 +92,4 @@ class CommandExecutionSerializer(serializers.ModelSerializer): @staticmethod def get_log_url(obj): return reverse('api-ops:celery-task-log', kwargs={'pk': obj.id}) + diff --git a/jumpserver/jumpserver/apps/ops/serializers/celery.py b/jumpserver/jumpserver/apps/ops/serializers/celery.py new file mode 100644 index 0000000000000000000000000000000000000000..8015c2482c4b27e82ff7a44e018abd9a8f79f35c --- /dev/null +++ b/jumpserver/jumpserver/apps/ops/serializers/celery.py @@ -0,0 +1,29 @@ +# ~*~ coding: utf-8 ~*~ +from __future__ import unicode_literals +from rest_framework import serializers + +from django_celery_beat.models import PeriodicTask + +__all__ = [ + 'CeleryResultSerializer', 'CeleryTaskSerializer', + 'CeleryPeriodTaskSerializer' +] + + +class CeleryResultSerializer(serializers.Serializer): + id = serializers.UUIDField() + result = serializers.JSONField() + state = serializers.CharField(max_length=16) + + +class CeleryTaskSerializer(serializers.Serializer): + pass + + +class CeleryPeriodTaskSerializer(serializers.ModelSerializer): + class Meta: + model = PeriodicTask + fields = [ + 'name', 'task', 'enabled', 'description', + 'last_run_at', 'total_run_count' + ] diff --git a/jumpserver/jumpserver/apps/ops/tasks.py b/jumpserver/jumpserver/apps/ops/tasks.py index 9e582959fb60bd61737ad6a3d17a88638a69e4f0..9746fdbe7c71053c644cc0b1f2575a9e27e1b9c1 100644 --- a/jumpserver/jumpserver/apps/ops/tasks.py +++ b/jumpserver/jumpserver/apps/ops/tasks.py @@ -1,21 +1,22 @@ # coding: utf-8 import os import subprocess -import datetime import time from django.conf import settings from celery import shared_task, subtask from celery.exceptions import SoftTimeLimitExceeded from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ -from common.utils import get_logger, get_object_or_none +from common.utils import get_logger, get_object_or_none, get_disk_usage from .celery.decorator import ( register_as_period_task, after_app_shutdown_clean_periodic, after_app_ready_start ) from .celery.utils import create_or_update_celery_periodic_tasks from .models import Task, CommandExecution, CeleryTask +from .utils import send_server_performance_mail logger = get_logger(__file__) @@ -59,7 +60,7 @@ def run_command_execution(cid, **kwargs): @shared_task @after_app_shutdown_clean_periodic -@register_as_period_task(interval=3600*24) +@register_as_period_task(interval=3600*24, description=_("Clean task history period")) def clean_tasks_adhoc_period(): logger.debug("Start clean task adhoc and run history") tasks = Task.objects.all() @@ -72,7 +73,7 @@ def clean_tasks_adhoc_period(): @shared_task @after_app_shutdown_clean_periodic -@register_as_period_task(interval=3600*24) +@register_as_period_task(interval=3600*24, description=_("Clean celery log period")) def clean_celery_tasks_period(): expire_days = 30 logger.debug("Start clean celery task history") @@ -103,6 +104,19 @@ def create_or_update_registered_periodic_tasks(): create_or_update_celery_periodic_tasks(task) +@shared_task +@register_as_period_task(interval=3600) +def check_server_performance_period(): + usages = get_disk_usage() + usages = {path: usage for path, usage in usages.items() + if not path.startswith('/etc')} + + for path, usage in usages.items(): + if usage.percent > 80: + send_server_performance_mail(path, usage, usages) + return + + @shared_task(queue="ansible") def hello(name, callback=None): import time diff --git a/jumpserver/jumpserver/apps/ops/templates/ops/adhoc_detail.html b/jumpserver/jumpserver/apps/ops/templates/ops/adhoc_detail.html index e3a13b54827c4ceaedf9fd62f77bd28c7ea7855e..777207da4ce39ba4296612497ebff0e12428a5b9 100644 --- a/jumpserver/jumpserver/apps/ops/templates/ops/adhoc_detail.html +++ b/jumpserver/jumpserver/apps/ops/templates/ops/adhoc_detail.html @@ -3,9 +3,7 @@ {% load i18n %} {% block custom_head_css_js %} - - {% endblock %} {% block content %} @@ -80,7 +78,7 @@ {% endif %} {% trans 'Become' %} - {{ object.become.user }} + {{ object.become_display }} {% trans 'Created by' %} diff --git a/jumpserver/jumpserver/apps/ops/templates/ops/adhoc_history.html b/jumpserver/jumpserver/apps/ops/templates/ops/adhoc_history.html index 11c52ae44afe6fbd8a63979e631d371907986678..8272279d4b566347bc2a76f37e871d9b2b7ebafc 100644 --- a/jumpserver/jumpserver/apps/ops/templates/ops/adhoc_history.html +++ b/jumpserver/jumpserver/apps/ops/templates/ops/adhoc_history.html @@ -3,9 +3,7 @@ {% load i18n %} {% block custom_head_css_js %} - - {% endblock %} {% block content %} diff --git a/jumpserver/jumpserver/apps/ops/templates/ops/adhoc_history_detail.html b/jumpserver/jumpserver/apps/ops/templates/ops/adhoc_history_detail.html index ccc4fce4c399d990e9df626605c9411bfd140184..88dde9ec2944106437d8c2407342a8b791acea4e 100644 --- a/jumpserver/jumpserver/apps/ops/templates/ops/adhoc_history_detail.html +++ b/jumpserver/jumpserver/apps/ops/templates/ops/adhoc_history_detail.html @@ -3,9 +3,7 @@ {% load i18n %} {% block custom_head_css_js %} - - {% endblock %} {% block content %} diff --git a/jumpserver/jumpserver/apps/ops/templates/ops/command_execution_create.html b/jumpserver/jumpserver/apps/ops/templates/ops/command_execution_create.html index cd1867c74b0c3aac0512642ac4710342d723e27e..8912cbe71f6b06f7f7b8a200932d939cdcae5bd2 100644 --- a/jumpserver/jumpserver/apps/ops/templates/ops/command_execution_create.html +++ b/jumpserver/jumpserver/apps/ops/templates/ops/command_execution_create.html @@ -4,25 +4,17 @@ {% load bootstrap3 %} {% block custom_head_css_js %} - + - - - - - + + + + - {% endblock %} diff --git a/jumpserver/jumpserver/apps/ops/templates/ops/task_adhoc.html b/jumpserver/jumpserver/apps/ops/templates/ops/task_adhoc.html index 866ef4255b8e92706cb3bc3051c9c02c41a69235..49035c7254dbba47b6e0754477067388622ed905 100644 --- a/jumpserver/jumpserver/apps/ops/templates/ops/task_adhoc.html +++ b/jumpserver/jumpserver/apps/ops/templates/ops/task_adhoc.html @@ -3,9 +3,7 @@ {% load i18n %} {% block custom_head_css_js %} - - {% endblock %} {% block content %} @@ -105,7 +103,7 @@ $(document).ready(function () { if (!cellData) { $(td).html("") } else { - $(td).html(cellData.user) + $(td).html(cellData) } }}, {targets: 6, createdCell: function (td, cellData) { @@ -120,8 +118,12 @@ $(document).ready(function () { }} ], ajax_url: '{% url "api-ops:adhoc-list" %}?task={{ object.pk }}', - columns: [{data: function(){return ""}}, {data: "short_id" }, {data: "hosts", orderable:false}, {data: "pattern", orderable:false}, - {data: "run_as"}, {data: "become", orderable:false}, {data: "date_created"}, {data: "id", orderable:false}] + columns: [ + {data: function(){return ""}}, {data: "short_id"}, + {data: "hosts", orderable:false}, {data: "pattern", orderable:false}, + {data: "run_as"}, {data: "become_display", orderable:false}, + {data: "date_created"}, {data: "id", orderable:false} + ] }; jumpserver.initDataTable(options); }).on('click', '.celery-task-log', function () { diff --git a/jumpserver/jumpserver/apps/ops/templates/ops/task_detail.html b/jumpserver/jumpserver/apps/ops/templates/ops/task_detail.html index c29fc2c4ee40e16fe6f2b99bd576a536a3450d3d..d2e46cc3510b32457464eb7b101b4a30d2a5e5ef 100644 --- a/jumpserver/jumpserver/apps/ops/templates/ops/task_detail.html +++ b/jumpserver/jumpserver/apps/ops/templates/ops/task_detail.html @@ -3,9 +3,7 @@ {% load i18n %} {% block custom_head_css_js %} - - {% endblock %} @@ -82,11 +80,23 @@ {% trans 'Is finished' %}: - {{ object.latest_history.is_finished|yesno:"Yes,No,Unkown" }} + + {% if object.latest_history.is_finished %} + {% trans 'Yes' %} + {% else %} + {% trans 'No' %} + {% endif %} + {% trans 'Is success ' %}: - {{ object.latest_history.is_success|yesno:"Yes,No,Unkown" }} + + {% if object.latest_history.is_success %} + {% trans 'Yes' %} + {% else %} + {% trans 'No' %} + {% endif %} + {% trans 'Contents' %}: diff --git a/jumpserver/jumpserver/apps/ops/templates/ops/task_history.html b/jumpserver/jumpserver/apps/ops/templates/ops/task_history.html index dea4c93240f330849ec5b31b022f3e16af04c8e7..5121a8045cfdbacc1295ccfe48087cb74110bf2e 100644 --- a/jumpserver/jumpserver/apps/ops/templates/ops/task_history.html +++ b/jumpserver/jumpserver/apps/ops/templates/ops/task_history.html @@ -3,9 +3,7 @@ {% load i18n %} {% block custom_head_css_js %} - - {% endblock %} {% block content %} diff --git a/jumpserver/jumpserver/apps/ops/templates/ops/task_list.html b/jumpserver/jumpserver/apps/ops/templates/ops/task_list.html index 83da620712a01f00f6e63c2774ab7056c37317e4..0d2095d78fd03ad3c2fd9015d14dd04460f772b0 100644 --- a/jumpserver/jumpserver/apps/ops/templates/ops/task_list.html +++ b/jumpserver/jumpserver/apps/ops/templates/ops/task_list.html @@ -10,7 +10,6 @@ {% trans 'Name' %} {% trans 'Run times' %} - {% trans 'Versions' %} {% trans 'Hosts' %} {% trans 'Success' %} {% trans 'Date' %} @@ -36,34 +35,40 @@ $(document).ready(function () { $(td).html(innerHtml); }}, {targets: 2, createdCell: function (td, cellData) { + var summary = cellData ? cellData.stat : {failed: 0, success: 0, total: 0}; var innerHtml = 'failed/success/total'; - if (cellData) { - innerHtml = innerHtml.replace('failed', cellData.failed) - .replace('success', cellData.success) - .replace('total', cellData.total); - $(td).html(innerHtml); - } else { - $(td).html('') - } + innerHtml = innerHtml.replace('failed', summary.failed) + .replace('success', summary.success) + .replace('total', summary.total); + $(td).html(innerHtml); }}, - {targets: 5, createdCell: function (td, cellData) { + {targets: 3, createdCell: function (td, cellData) { + var hostsAmount = cellData ? cellData.hosts_amount : 0; + $(td).html(hostsAmount) + }}, + {targets: 4, createdCell: function (td, cellData) { var successBtn = ''; var failedBtn = ''; - if (cellData) { + if (cellData && cellData.is_success) { $(td).html(successBtn) } else { $(td).html(failedBtn) } }}, - {targets: 6, createdCell: function (td, cellData) { - $(td).html(toSafeLocalDateStr(cellData)); + {targets: 5, createdCell: function (td, cellData) { + if (cellData) { + $(td).html(toSafeLocalDateStr(cellData.date_start)); + } else { + $(td).html(''); + } }}, - {targets: 7, createdCell: function (td, cellData) { + {targets: 6, createdCell: function (td, cellData) { + cellData = cellData ? cellData.timedelta : 0; var delta = readableSecond(cellData); $(td).html(delta); }}, { - targets: 8, + targets: 7, createdCell: function (td, cellData, rowData) { var runBtn = '{% trans "Run" %} '.replace('ID', cellData); var delBtn = '{% trans "Delete" %}'.replace('ID', cellData); @@ -73,10 +78,11 @@ $(document).ready(function () { ], ajax_url: '{% url "api-ops:task-list" %}', columns: [ - {data: "id"}, {data: "name", className: "text-left"}, {data: "history_summary", orderable: false}, - {data: "versions", orderable: false}, {data: "assets_amount", orderable: false}, - {data: "is_success", orderable: false}, {data: "date_updated"}, - {data: "timedelta", orderable:false}, {data: "id", orderable: false}, + {data: "id"}, {data: "name", className: "text-left"}, + {data: "latest_history", orderable: false}, + {data: "latest_history", orderable: false}, + {data: "latest_history", orderable: false}, {data: "latest_history"}, + {data: "latest_history", orderable:false}, {data: "id", orderable: false}, ], order: [], op_html: $('#actions').html() diff --git a/jumpserver/jumpserver/apps/ops/urls/api_urls.py b/jumpserver/jumpserver/apps/ops/urls/api_urls.py index 5f955540dbddea6cee9a75deb2bec00bc85aa3ed..cc242f649badd13418e7c47c97efe0c30a4909e3 100644 --- a/jumpserver/jumpserver/apps/ops/urls/api_urls.py +++ b/jumpserver/jumpserver/apps/ops/urls/api_urls.py @@ -13,6 +13,7 @@ router.register(r'tasks', api.TaskViewSet, 'task') router.register(r'adhoc', api.AdHocViewSet, 'adhoc') router.register(r'history', api.AdHocRunHistoryViewSet, 'history') router.register(r'command-executions', api.CommandExecutionViewSet, 'command-execution') +router.register(r'celery/period-tasks', api.CeleryPeriodTaskViewSet, 'celery-period-task') urlpatterns = [ path('tasks//run/', api.TaskRun.as_view(), name='task-run'), diff --git a/jumpserver/jumpserver/apps/ops/urls/view_urls.py b/jumpserver/jumpserver/apps/ops/urls/view_urls.py index f8428a667f6385b51d555fa9627fcec7d09b01e6..13759c3f282a71e10e1d9075035ef534f61d5dad 100644 --- a/jumpserver/jumpserver/apps/ops/urls/view_urls.py +++ b/jumpserver/jumpserver/apps/ops/urls/view_urls.py @@ -20,5 +20,5 @@ urlpatterns = [ path('celery/task//log/', views.CeleryTaskLogView.as_view(), name='celery-task-log'), path('command-execution/', views.CommandExecutionListView.as_view(), name='command-execution-list'), - path('command-execution/start/', views.CommandExecutionStartView.as_view(), name='command-execution-start'), + path('command-execution/create/', views.CommandExecutionCreateView.as_view(), name='command-execution-create'), ] diff --git a/jumpserver/jumpserver/apps/ops/utils.py b/jumpserver/jumpserver/apps/ops/utils.py index 70cba27ba2301a476bac365cdf2292f3199f3691..3322890f2b82fea5e19d48fa2650fce96a799656 100644 --- a/jumpserver/jumpserver/apps/ops/utils.py +++ b/jumpserver/jumpserver/apps/ops/utils.py @@ -1,6 +1,8 @@ # ~*~ coding: utf-8 ~*~ from django.utils.translation import ugettext_lazy as _ + from common.utils import get_logger, get_object_or_none +from common.tasks import send_mail_async from orgs.utils import set_to_root_org from .models import Task, AdHoc @@ -56,4 +58,14 @@ def update_or_create_ansible_task( return task, created +def send_server_performance_mail(path, usage, usages): + from users.models import User + subject = _("Disk used more than 80%: {} => {}").format(path, usage.percent) + message = subject + admins = User.objects.filter(role=User.ROLE_ADMIN) + recipient_list = [u.email for u in admins if u.email] + logger.info(subject) + send_mail_async(subject, message, recipient_list, html_message=message) + + diff --git a/jumpserver/jumpserver/apps/ops/views/celery.py b/jumpserver/jumpserver/apps/ops/views/celery.py index 1fcd18d082b736e4644d3fb8e70580c3fc51e5fb..9ae2d975581ee56d1f7b9413492e8748de8c10a4 100644 --- a/jumpserver/jumpserver/apps/ops/views/celery.py +++ b/jumpserver/jumpserver/apps/ops/views/celery.py @@ -17,6 +17,6 @@ class CeleryTaskLogView(PermissionsMixin, TemplateView): context = super().get_context_data(**kwargs) context.update({ 'task_id': self.kwargs.get('pk'), - 'ws_port': settings.CONFIG.WS_LISTEN_PORT + 'ws_port': settings.WS_LISTEN_PORT }) return context diff --git a/jumpserver/jumpserver/apps/ops/views/command.py b/jumpserver/jumpserver/apps/ops/views/command.py index 0824b4b9359be6c81c8972e7d6d2a5607f2199df..87e0528c69dc4444d194f18bc5773c3cd9609f6a 100644 --- a/jumpserver/jumpserver/apps/ops/views/command.py +++ b/jumpserver/jumpserver/apps/ops/views/command.py @@ -15,7 +15,7 @@ from ..forms import CommandExecutionForm __all__ = [ - 'CommandExecutionListView', 'CommandExecutionStartView' + 'CommandExecutionListView', 'CommandExecutionCreateView' ] @@ -55,7 +55,7 @@ class CommandExecutionListView(PermissionsMixin, DatetimeSearchMixin, ListView): return super().get_context_data(**kwargs) -class CommandExecutionStartView(PermissionsMixin, TemplateView): +class CommandExecutionCreateView(PermissionsMixin, TemplateView): template_name = 'ops/command_execution_create.html' form_class = CommandExecutionForm permission_classes = [IsValidUser] @@ -80,7 +80,7 @@ class CommandExecutionStartView(PermissionsMixin, TemplateView): 'action': _('Command execution'), 'form': self.get_form(), 'system_users': system_users, - 'ws_port': settings.CONFIG.WS_LISTEN_PORT + 'ws_port': settings.WS_LISTEN_PORT } kwargs.update(context) return super().get_context_data(**kwargs) diff --git a/jumpserver/jumpserver/apps/orgs/mixins/api.py b/jumpserver/jumpserver/apps/orgs/mixins/api.py index 8fb7f30dd56c4397b39decac4b147edc4c83a223..64314a3f8ea4d523d7571beffa8aced6b9bd78fc 100644 --- a/jumpserver/jumpserver/apps/orgs/mixins/api.py +++ b/jumpserver/jumpserver/apps/orgs/mixins/api.py @@ -46,7 +46,11 @@ class OrgModelViewSet(CommonApiMixin, OrgQuerySetMixin, ModelViewSet): class OrgBulkModelViewSet(CommonApiMixin, OrgQuerySetMixin, BulkModelViewSet): def allow_bulk_destroy(self, qs, filtered): - if qs.count() <= filtered.count(): + qs_count = qs.count() + filtered_count = filtered.count() + if filtered_count == 1: + return True + if qs_count <= filtered_count: return False if self.request.query_params.get('spm', ''): return True diff --git a/jumpserver/jumpserver/apps/orgs/mixins/models.py b/jumpserver/jumpserver/apps/orgs/mixins/models.py index 4ffa24c2a555658b8353b194b7623ba160b9799b..30429b874f553ae74e82c0dfa3626cdfa5c5c1bd 100644 --- a/jumpserver/jumpserver/apps/orgs/mixins/models.py +++ b/jumpserver/jumpserver/apps/orgs/mixins/models.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # -import traceback from django.db import models from django.utils.translation import ugettext_lazy as _ from django.core.exceptions import ValidationError @@ -9,7 +8,7 @@ from django.core.exceptions import ValidationError from common.utils import get_logger from ..utils import ( set_current_org, get_current_org, current_org, - get_org_filters + filter_org_queryset ) from ..models import Organization @@ -20,37 +19,24 @@ __all__ = [ ] -class OrgQuerySet(models.QuerySet): - pass - - class OrgManager(models.Manager): - def get_queryset(self): - queryset = super().get_queryset() - kwargs = get_org_filters() - if kwargs: - return queryset.filter(**kwargs) - return queryset - def set_current_org(self, org): - if isinstance(org, str): - org = Organization.get_instance(org) - set_current_org(org) - return self + def get_queryset(self): + queryset = super(OrgManager, self).get_queryset() + return filter_org_queryset(queryset) def all(self): - # print("Call all: {}".format(current_org)) - # - # lines = traceback.format_stack() - # print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>") - # for line in lines[-10:-1]: - # print(line) - # print("<<<<<<<<<<<<<<<<<<<<<<<<<<<<") if not current_org: msg = 'You can `objects.set_current_org(org).all()` then run it' return self else: - return super().all() + return super(OrgManager, self).all() + + def set_current_org(self, org): + if isinstance(org, str): + org = Organization.get_instance(org) + set_current_org(org) + return self class OrgModelMixin(models.Model): diff --git a/jumpserver/jumpserver/apps/orgs/models.py b/jumpserver/jumpserver/apps/orgs/models.py index f337864fc4f1a61957763832e29cd9c4142e5b0e..a9040b3785afdf129b98f8f95ce41e291523960f 100644 --- a/jumpserver/jumpserver/apps/orgs/models.py +++ b/jumpserver/jumpserver/apps/orgs/models.py @@ -4,7 +4,7 @@ from django.conf import settings from django.db import models from django.utils.translation import ugettext_lazy as _ -from common.utils import is_uuid +from common.utils import is_uuid, lazyproperty class Organization(models.Model): @@ -72,7 +72,8 @@ class Organization(models.Model): org = cls.default() if default else None return org - def get_org_users(self): + @lazyproperty + def org_users(self): from users.models import User if self.is_real(): return self.users.all() @@ -81,18 +82,37 @@ class Organization(models.Model): users = users.filter(related_user_orgs__isnull=True) return users - def get_org_admins(self): + def get_org_users(self): + return self.org_users + + @lazyproperty + def org_admins(self): from users.models import User if self.is_real(): return self.admins.all() return User.objects.filter(role=User.ROLE_ADMIN) - def get_org_auditors(self): + def get_org_admins(self): + return self.org_admins + + def org_id(self): + if self.is_real(): + return self.id + elif self.is_root(): + return None + else: + return '' + + @lazyproperty + def org_auditors(self): from users.models import User if self.is_real(): return self.auditors.all() return User.objects.filter(role=User.ROLE_AUDITOR) + def get_org_auditors(self): + return self.org_auditors + def get_org_members(self, exclude=()): from users.models import User members = User.objects.none() diff --git a/jumpserver/jumpserver/apps/orgs/serializers.py b/jumpserver/jumpserver/apps/orgs/serializers.py index 3ac7da7327e66acc24a887f5e7f3fe23740d4a50..281b9ec75ea3b075da3c4b1631aba733d9e2195e 100644 --- a/jumpserver/jumpserver/apps/orgs/serializers.py +++ b/jumpserver/jumpserver/apps/orgs/serializers.py @@ -4,10 +4,6 @@ from rest_framework.serializers import ModelSerializer from rest_framework import serializers from users.models import User, UserGroup from assets.models import Asset, Domain, AdminUser, SystemUser, Label -from assets.const import ( - GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_PATTERN, - GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_ERROR_MSG -) from perms.models import AssetPermission from common.serializers import AdaptedBulkListSerializer from .utils import set_current_org, get_current_org @@ -22,15 +18,6 @@ class OrgSerializer(ModelSerializer): fields = '__all__' read_only_fields = ['created_by', 'date_created'] - @staticmethod - def validate_name(name): - pattern = GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_PATTERN - res = re.search(pattern, name) - if res is not None: - msg = GENERAL_FORBIDDEN_SPECIAL_CHARACTERS_ERROR_MSG - raise serializers.ValidationError(msg) - return name - class OrgReadSerializer(ModelSerializer): admins = serializers.SlugRelatedField(slug_field='name', many=True, read_only=True) diff --git a/jumpserver/jumpserver/apps/perms/api/__init__.py b/jumpserver/jumpserver/apps/perms/api/__init__.py index 00dd28776bd9b8680d9373c265f510091a77d091..61cbd7d58128b73ea071ebb84bae3b3e56d13086 100644 --- a/jumpserver/jumpserver/apps/perms/api/__init__.py +++ b/jumpserver/jumpserver/apps/perms/api/__init__.py @@ -3,6 +3,10 @@ from .asset_permission import * from .user_permission import * +from .asset_permission_relation import * from .user_group_permission import * from .remote_app_permission import * from .user_remote_app_permission import * +from .database_app_permission import * +from .database_app_permission_relation import * +from .user_database_app_permission import * diff --git a/jumpserver/jumpserver/apps/perms/api/asset_permission.py b/jumpserver/jumpserver/apps/perms/api/asset_permission.py index 0243f8f1b8d5fa102c1e657f83d057a4b6255690..724b1d1976496ea463c4cf2e5083855e274321aa 100644 --- a/jumpserver/jumpserver/apps/perms/api/asset_permission.py +++ b/jumpserver/jumpserver/apps/perms/api/asset_permission.py @@ -2,12 +2,9 @@ # from django.db.models import Q -from rest_framework.views import Response -from django.shortcuts import get_object_or_404 from common.permissions import IsOrgAdmin from orgs.mixins.api import OrgModelViewSet -from orgs.mixins import generics from common.utils import get_object_or_none from ..models import AssetPermission from ..hands import ( @@ -17,9 +14,7 @@ from .. import serializers __all__ = [ - 'AssetPermissionViewSet', 'AssetPermissionRemoveUserApi', - 'AssetPermissionAddUserApi', 'AssetPermissionRemoveAssetApi', - 'AssetPermissionAddAssetApi', 'AssetPermissionAssetsApi', + 'AssetPermissionViewSet', ] @@ -28,7 +23,10 @@ class AssetPermissionViewSet(OrgModelViewSet): 资产授权列表的增删改查api """ model = AssetPermission - serializer_class = serializers.AssetPermissionCreateUpdateSerializer + serializer_classes = { + 'default': serializers.AssetPermissionCreateUpdateSerializer, + 'display': serializers.AssetPermissionListSerializer + } filter_fields = ['name'] permission_classes = (IsOrgAdmin,) @@ -38,11 +36,9 @@ class AssetPermissionViewSet(OrgModelViewSet): ) return queryset - def get_serializer_class(self): - if self.action in ("list", 'retrieve') and \ - self.request.query_params.get("display"): - return serializers.AssetPermissionListSerializer - return self.serializer_class + def is_query_all(self): + query_all = self.request.query_params.get('all', '1') == '1' + return query_all def filter_valid(self, queryset): valid_query = self.request.query_params.get('is_valid', None) @@ -81,7 +77,10 @@ class AssetPermissionViewSet(OrgModelViewSet): if not _nodes: return queryset.none() - nodes = set() + if not self.is_query_all(): + queryset = queryset.filter(nodes__in=_nodes) + return queryset + nodes = set(_nodes) for node in _nodes: nodes |= set(node.get_ancestors(with_self=True)) queryset = queryset.filter(nodes__in=nodes) @@ -101,6 +100,9 @@ class AssetPermissionViewSet(OrgModelViewSet): return queryset if not assets: return queryset.none() + if not self.is_query_all(): + queryset = queryset.filter(assets__in=assets) + return queryset inherit_all_nodes = set() inherit_nodes_keys = assets.all().values_list('nodes__key', flat=True) @@ -117,7 +119,6 @@ class AssetPermissionViewSet(OrgModelViewSet): def filter_user(self, queryset): user_id = self.request.query_params.get('user_id') username = self.request.query_params.get('username') - query_group = self.request.query_params.get('all') if user_id: user = get_object_or_none(User, pk=user_id) elif username: @@ -126,14 +127,14 @@ class AssetPermissionViewSet(OrgModelViewSet): return queryset if not user: return queryset.none() - kwargs = {} - args = [] - if query_group: - groups = user.groups.all() - args.append(Q(users=user) | Q(user_groups__in=groups)) - else: - kwargs["users"] = user - return queryset.filter(*args, **kwargs).distinct() + if not self.is_query_all(): + queryset = queryset.filter(users=user) + return queryset + groups = user.groups.all() + queryset = queryset.filter( + Q(users=user) | Q(user_groups__in=groups) + ).distinct() + return queryset def filter_user_group(self, queryset): user_group_id = self.request.query_params.get('user_group_id') @@ -167,99 +168,3 @@ class AssetPermissionViewSet(OrgModelViewSet): queryset = self.filter_user_group(queryset) queryset = queryset.distinct() return queryset - - -class AssetPermissionRemoveUserApi(generics.RetrieveUpdateAPIView): - """ - 将用户从授权中移除,Detail页面会调用 - """ - model = AssetPermission - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.AssetPermissionUpdateUserSerializer - - def update(self, request, *args, **kwargs): - perm = self.get_object() - serializer = self.serializer_class(data=request.data) - if serializer.is_valid(): - users = serializer.validated_data.get('users') - if users: - perm.users.remove(*tuple(users)) - perm.save() - return Response({"msg": "ok"}) - else: - return Response({"error": serializer.errors}) - - -class AssetPermissionAddUserApi(generics.RetrieveUpdateAPIView): - model = AssetPermission - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.AssetPermissionUpdateUserSerializer - - def update(self, request, *args, **kwargs): - perm = self.get_object() - serializer = self.serializer_class(data=request.data) - if serializer.is_valid(): - users = serializer.validated_data.get('users') - if users: - perm.users.add(*tuple(users)) - perm.save() - return Response({"msg": "ok"}) - else: - return Response({"error": serializer.errors}) - - -class AssetPermissionRemoveAssetApi(generics.RetrieveUpdateAPIView): - """ - 将用户从授权中移除,Detail页面会调用 - """ - model = AssetPermission - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.AssetPermissionUpdateAssetSerializer - - def update(self, request, *args, **kwargs): - perm = self.get_object() - serializer = self.serializer_class(data=request.data) - if serializer.is_valid(): - assets = serializer.validated_data.get('assets') - if assets: - perm.assets.remove(*tuple(assets)) - perm.save() - return Response({"msg": "ok"}) - else: - return Response({"error": serializer.errors}) - - -class AssetPermissionAddAssetApi(generics.RetrieveUpdateAPIView): - model = AssetPermission - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.AssetPermissionUpdateAssetSerializer - - def update(self, request, *args, **kwargs): - perm = self.get_object() - serializer = self.serializer_class(data=request.data) - if serializer.is_valid(): - assets = serializer.validated_data.get('assets') - if assets: - perm.assets.add(*tuple(assets)) - perm.save() - return Response({"msg": "ok"}) - else: - return Response({"error": serializer.errors}) - - -class AssetPermissionAssetsApi(generics.ListAPIView): - permission_classes = (IsOrgAdmin,) - serializer_class = serializers.AssetPermissionAssetsSerializer - filter_fields = ("hostname", "ip") - search_fields = filter_fields - - def get_object(self): - pk = self.kwargs.get('pk') - return get_object_or_404(AssetPermission, pk=pk) - - def get_queryset(self): - perm = self.get_object() - assets = perm.get_all_assets().only( - *self.serializer_class.Meta.only_fields - ) - return assets diff --git a/jumpserver/jumpserver/apps/perms/api/asset_permission_relation.py b/jumpserver/jumpserver/apps/perms/api/asset_permission_relation.py new file mode 100644 index 0000000000000000000000000000000000000000..3b68af7755cd058b26e8d9aa64ea3f37e51512fb --- /dev/null +++ b/jumpserver/jumpserver/apps/perms/api/asset_permission_relation.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +# +from rest_framework import generics +from django.db.models import F, Value +from django.db.models.functions import Concat +from django.shortcuts import get_object_or_404 + +from orgs.mixins.api import OrgBulkModelViewSet +from orgs.utils import current_org +from common.permissions import IsOrgAdmin +from .. import serializers +from .. import models + +__all__ = [ + 'AssetPermissionUserRelationViewSet', 'AssetPermissionUserGroupRelationViewSet', + 'AssetPermissionAssetRelationViewSet', 'AssetPermissionNodeRelationViewSet', + 'AssetPermissionSystemUserRelationViewSet', 'AssetPermissionAllAssetListApi', + 'AssetPermissionAllUserListApi', +] + + +class RelationMixin(OrgBulkModelViewSet): + def get_queryset(self): + queryset = self.model.objects.all() + org_id = current_org.org_id() + if org_id is not None: + queryset = queryset.filter(assetpermission__org_id=org_id) + queryset = queryset.annotate(assetpermission_display=F('assetpermission__name')) + return queryset + + +class AssetPermissionUserRelationViewSet(RelationMixin): + serializer_class = serializers.AssetPermissionUserRelationSerializer + model = models.AssetPermission.users.through + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', "user", "assetpermission", + ] + search_fields = ("user__name", "user__username", "assetpermission__name") + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset \ + .annotate(user_display=F('user__name')) + return queryset + + +class AssetPermissionAllUserListApi(generics.ListAPIView): + permission_classes = (IsOrgAdmin,) + serializer_class = serializers.AssetPermissionAllUserSerializer + filter_fields = ("username", "name") + search_fields = filter_fields + + def get_queryset(self): + pk = self.kwargs.get("pk") + perm = get_object_or_404(models.AssetPermission, pk=pk) + users = perm.get_all_users().only( + *self.serializer_class.Meta.only_fields + ) + return users + + +class AssetPermissionUserGroupRelationViewSet(RelationMixin): + serializer_class = serializers.AssetPermissionUserGroupRelationSerializer + model = models.AssetPermission.user_groups.through + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', "usergroup", "assetpermission" + ] + search_fields = ["usergroup__name", "assetpermission__name"] + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset \ + .annotate(usergroup_display=F('usergroup__name')) + return queryset + + +class AssetPermissionAssetRelationViewSet(RelationMixin): + serializer_class = serializers.AssetPermissionAssetRelationSerializer + model = models.AssetPermission.assets.through + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', 'asset', 'assetpermission', + ] + search_fields = ["id", "asset__hostname", "asset__ip", "assetpermission__name"] + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset \ + .annotate(asset_display=F('asset__hostname')) + return queryset + + +class AssetPermissionAllAssetListApi(generics.ListAPIView): + permission_classes = (IsOrgAdmin,) + serializer_class = serializers.AssetPermissionAllAssetSerializer + filter_fields = ("hostname", "ip") + search_fields = filter_fields + + def get_queryset(self): + pk = self.kwargs.get("pk") + perm = get_object_or_404(models.AssetPermission, pk=pk) + assets = perm.get_all_assets().only( + *self.serializer_class.Meta.only_fields + ) + return assets + + +class AssetPermissionNodeRelationViewSet(RelationMixin): + serializer_class = serializers.AssetPermissionNodeRelationSerializer + model = models.AssetPermission.nodes.through + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', 'node', 'assetpermission', + ] + search_fields = ["node__value", "assetpermission__name"] + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset \ + .annotate(node_key=F('node__key')) + return queryset + + +class AssetPermissionSystemUserRelationViewSet(RelationMixin): + serializer_class = serializers.AssetPermissionSystemUserRelationSerializer + model = models.AssetPermission.system_users.through + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', 'systemuser', 'assetpermission', + ] + search_fields = [ + "assetpermission__name", "systemuser__name", "systemuser__username" + ] + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset \ + .annotate(systemuser_display=Concat( + F('systemuser__name'), Value('('), F('systemuser__username'), + Value(')') + )) + return queryset diff --git a/jumpserver/jumpserver/apps/perms/api/database_app_permission.py b/jumpserver/jumpserver/apps/perms/api/database_app_permission.py new file mode 100644 index 0000000000000000000000000000000000000000..4b43d347f86bccebb4a7e74f7a4d372ad601894f --- /dev/null +++ b/jumpserver/jumpserver/apps/perms/api/database_app_permission.py @@ -0,0 +1,21 @@ +# coding: utf-8 +# + +from orgs.mixins.api import OrgBulkModelViewSet + +from .. import models, serializers +from common.permissions import IsOrgAdmin + + +__all__ = ['DatabaseAppPermissionViewSet'] + + +class DatabaseAppPermissionViewSet(OrgBulkModelViewSet): + model = models.DatabaseAppPermission + serializer_classes = { + 'default': serializers.DatabaseAppPermissionSerializer, + 'display': serializers.DatabaseAppPermissionListSerializer + } + filter_fields = ('name',) + search_fields = filter_fields + permission_classes = (IsOrgAdmin,) diff --git a/jumpserver/jumpserver/apps/perms/api/database_app_permission_relation.py b/jumpserver/jumpserver/apps/perms/api/database_app_permission_relation.py new file mode 100644 index 0000000000000000000000000000000000000000..32ab34355d05838b2acfb71a682314ee96ece5df --- /dev/null +++ b/jumpserver/jumpserver/apps/perms/api/database_app_permission_relation.py @@ -0,0 +1,132 @@ +# coding: utf-8 +# + +from rest_framework import generics +from django.db.models import F, Value +from django.db.models.functions import Concat +from django.shortcuts import get_object_or_404 + +from orgs.mixins.api import OrgBulkModelViewSet +from orgs.utils import current_org +from common.permissions import IsOrgAdmin +from .. import models, serializers + +__all__ = [ + 'DatabaseAppPermissionUserRelationViewSet', + 'DatabaseAppPermissionUserGroupRelationViewSet', + 'DatabaseAppPermissionAllUserListApi', + 'DatabaseAppPermissionDatabaseAppRelationViewSet', + 'DatabaseAppPermissionAllDatabaseAppListApi', + 'DatabaseAppPermissionSystemUserRelationViewSet', +] + + +class RelationMixin(OrgBulkModelViewSet): + def get_queryset(self): + queryset = self.model.objects.all() + org_id = current_org.org_id() + if org_id is not None: + queryset = queryset.filter(databaseapppermission__org_id=org_id) + queryset = queryset.annotate(databaseapppermission_display=F('databaseapppermission__name')) + return queryset + + +class DatabaseAppPermissionUserRelationViewSet(RelationMixin): + serializer_class = serializers.DatabaseAppPermissionUserRelationSerializer + model = models.DatabaseAppPermission.users.through + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', 'user', 'databaseapppermission' + ] + search_fields = ('user__name', 'user__username', 'databaseapppermission__name') + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.annotate(user_display=F('user__name')) + return queryset + + +class DatabaseAppPermissionUserGroupRelationViewSet(RelationMixin): + serializer_class = serializers.DatabaseAppPermissionUserGroupRelationSerializer + model = models.DatabaseAppPermission.user_groups.through + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', "usergroup", "databaseapppermission" + ] + search_fields = ["usergroup__name", "databaseapppermission__name"] + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset \ + .annotate(usergroup_display=F('usergroup__name')) + return queryset + + +class DatabaseAppPermissionAllUserListApi(generics.ListAPIView): + permission_classes = (IsOrgAdmin,) + serializer_class = serializers.DatabaseAppPermissionAllUserSerializer + filter_fields = ("username", "name") + search_fields = filter_fields + + def get_queryset(self): + pk = self.kwargs.get("pk") + perm = get_object_or_404(models.DatabaseAppPermission, pk=pk) + users = perm.get_all_users().only( + *self.serializer_class.Meta.only_fields + ) + return users + + +class DatabaseAppPermissionDatabaseAppRelationViewSet(RelationMixin): + serializer_class = serializers.DatabaseAppPermissionDatabaseAppRelationSerializer + model = models.DatabaseAppPermission.database_apps.through + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', 'databaseapp', 'databaseapppermission', + ] + search_fields = [ + "id", "databaseapp__name", "databaseapppermission__name" + ] + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset \ + .annotate(databaseapp_display=F('databaseapp__name')) + return queryset + + +class DatabaseAppPermissionAllDatabaseAppListApi(generics.ListAPIView): + permission_classes = (IsOrgAdmin,) + serializer_class = serializers.DatabaseAppPermissionAllDatabaseAppSerializer + filter_fields = ("name",) + search_fields = filter_fields + + def get_queryset(self): + pk = self.kwargs.get("pk") + perm = get_object_or_404(models.DatabaseAppPermission, pk=pk) + database_apps = perm.get_all_database_apps().only( + *self.serializer_class.Meta.only_fields + ) + return database_apps + + +class DatabaseAppPermissionSystemUserRelationViewSet(RelationMixin): + serializer_class = serializers.DatabaseAppPermissionSystemUserRelationSerializer + model = models.DatabaseAppPermission.system_users.through + permission_classes = (IsOrgAdmin,) + filterset_fields = [ + 'id', 'systemuser', 'databaseapppermission' + ] + search_fields = [ + 'databaseapppermission__name', 'systemuser__name', 'systemuser__username' + ] + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.annotate( + systemuser_display=Concat( + F('systemuser__name'), Value('('), F('systemuser__username'), + Value(')') + ) + ) + return queryset diff --git a/jumpserver/jumpserver/apps/perms/api/remote_app_permission.py b/jumpserver/jumpserver/apps/perms/api/remote_app_permission.py index 6ced7f0ae289f24205cbf08c704f19060d7fda07..b7fa6de1991826092ead0ac0a5fdb0d1c78cfedc 100644 --- a/jumpserver/jumpserver/apps/perms/api/remote_app_permission.py +++ b/jumpserver/jumpserver/apps/perms/api/remote_app_permission.py @@ -11,6 +11,7 @@ from ..serializers import ( RemoteAppPermissionSerializer, RemoteAppPermissionUpdateUserSerializer, RemoteAppPermissionUpdateRemoteAppSerializer, + RemoteAppPermissionListSerializer, ) @@ -25,7 +26,10 @@ class RemoteAppPermissionViewSet(OrgModelViewSet): model = RemoteAppPermission filter_fields = ('name', ) search_fields = filter_fields - serializer_class = RemoteAppPermissionSerializer + serializer_classes = { + 'default': RemoteAppPermissionSerializer, + 'display': RemoteAppPermissionListSerializer, + } permission_classes = (IsOrgAdmin,) diff --git a/jumpserver/jumpserver/apps/perms/api/user_database_app_permission.py b/jumpserver/jumpserver/apps/perms/api/user_database_app_permission.py new file mode 100644 index 0000000000000000000000000000000000000000..3a973b8c14024ac331dc327fa8f51c4d5ad71344 --- /dev/null +++ b/jumpserver/jumpserver/apps/perms/api/user_database_app_permission.py @@ -0,0 +1,127 @@ +# coding: utf-8 +# + +import uuid +from django.shortcuts import get_object_or_404 +from rest_framework.views import APIView, Response +from common.permissions import IsOrgAdminOrAppUser, IsValidUser +from common.tree import TreeNodeSerializer +from orgs.mixins import generics +from users.models import User, UserGroup +from applications.serializers import DatabaseAppSerializer +from applications.models import DatabaseApp +from assets.models import SystemUser +from .. import utils, serializers +from .mixin import UserPermissionMixin + +__all__ = [ + 'UserGrantedDatabaseAppsApi', + 'UserGrantedDatabaseAppsAsTreeApi', + 'UserGroupGrantedDatabaseAppsApi', + 'ValidateUserDatabaseAppPermissionApi', + 'UserGrantedDatabaseAppSystemUsersApi', +] + + +class UserGrantedDatabaseAppsApi(generics.ListAPIView): + permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = DatabaseAppSerializer + filter_fields = ['id', 'name'] + search_fields = ['name'] + + def get_object(self): + user_id = self.kwargs.get('pk', '') + if user_id: + user = get_object_or_404(User, id=user_id) + else: + user = self.request.user + return user + + def get_queryset(self): + util = utils.DatabaseAppPermissionUtil(self.get_object()) + queryset = util.get_database_apps() + return queryset + + def get_permissions(self): + if self.kwargs.get('pk') is None: + self.permission_classes = (IsValidUser,) + return super().get_permissions() + + +class UserGrantedDatabaseAppsAsTreeApi(UserGrantedDatabaseAppsApi): + serializer_class = TreeNodeSerializer + permission_classes = (IsOrgAdminOrAppUser,) + + def get_serializer(self, database_apps, *args, **kwargs): + if database_apps is None: + database_apps = [] + only_database_app = self.request.query_params.get('only', '0') == '1' + tree_root = None + data = [] + if not only_database_app: + tree_root = utils.construct_database_apps_tree_root() + data.append(tree_root) + for database_app in database_apps: + node = utils.parse_database_app_to_tree_node(tree_root, database_app) + data.append(node) + data.sort() + return super().get_serializer(data, many=True) + + +class UserGrantedDatabaseAppSystemUsersApi(UserPermissionMixin, generics.ListAPIView): + permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = serializers.DatabaseAppSystemUserSerializer + only_fields = serializers.DatabaseAppSystemUserSerializer.Meta.only_fields + + def get_queryset(self): + util = utils.DatabaseAppPermissionUtil(self.obj) + database_app_id = self.kwargs.get('database_app_id') + database_app = get_object_or_404(DatabaseApp, id=database_app_id) + system_users = util.get_database_app_system_users(database_app) + return system_users + + +# Validate + +class ValidateUserDatabaseAppPermissionApi(APIView): + permission_classes = (IsOrgAdminOrAppUser,) + + def get(self, request, *args, **kwargs): + user_id = request.query_params.get('user_id', '') + database_app_id = request.query_params.get('database_app_id', '') + system_user_id = request.query_params.get('system_user_id', '') + + try: + user_id = uuid.UUID(user_id) + database_app_id = uuid.UUID(database_app_id) + system_user_id = uuid.UUID(system_user_id) + except ValueError: + return Response({'msg': False}, status=403) + + user = get_object_or_404(User, id=user_id) + database_app = get_object_or_404(DatabaseApp, id=database_app_id) + system_user = get_object_or_404(SystemUser, id=system_user_id) + + util = utils.DatabaseAppPermissionUtil(user) + system_users = util.get_database_app_system_users(database_app) + if system_user in system_users: + return Response({'msg': True}, status=200) + + return Response({'msg': False}, status=403) + + +# UserGroup + +class UserGroupGrantedDatabaseAppsApi(generics.ListAPIView): + permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = DatabaseAppSerializer + + def get_queryset(self): + queryset = [] + user_group_id = self.kwargs.get('pk') + if not user_group_id: + return queryset + user_group = get_object_or_404(UserGroup, id=user_group_id) + util = utils.DatabaseAppPermissionUtil(user_group) + queryset = util.get_database_apps() + return queryset diff --git a/jumpserver/jumpserver/apps/perms/api/user_permission/common.py b/jumpserver/jumpserver/apps/perms/api/user_permission/common.py index 7e4271af82dc1aa63dafd20497e6b986e5bf9647..12469d3da44bdba382fe1dcbd380f8b0bedb8e04 100644 --- a/jumpserver/jumpserver/apps/perms/api/user_permission/common.py +++ b/jumpserver/jumpserver/apps/perms/api/user_permission/common.py @@ -59,6 +59,9 @@ class GetUserAssetPermissionActionsApi(UserAssetPermissionMixin, class ValidateUserAssetPermissionApi(UserAssetPermissionMixin, APIView): permission_classes = (IsOrgAdminOrAppUser,) + def get_cache_policy(self): + return 0 + def get_obj(self): user_id = self.request.query_params.get('user_id', '') user = get_object_or_404(User, id=user_id) @@ -81,6 +84,8 @@ class ValidateUserAssetPermissionApi(UserAssetPermissionMixin, APIView): system_users_actions = self.util.get_asset_system_users_with_actions( asset) actions = system_users_actions.get(system_user) + if actions is None: + return Response({'msg': False}, status=403) if action_name in Action.value_to_choices(actions): return Response({'msg': True}, status=200) return Response({'msg': False}, status=403) diff --git a/jumpserver/jumpserver/apps/perms/api/user_permission/mixin.py b/jumpserver/jumpserver/apps/perms/api/user_permission/mixin.py index 4a004e0d1c9baa38c45794f4493abad89eaa5f7c..bbc926ffe1ab951f453c02a9077a7cf4d1f3c14c 100644 --- a/jumpserver/jumpserver/apps/perms/api/user_permission/mixin.py +++ b/jumpserver/jumpserver/apps/perms/api/user_permission/mixin.py @@ -10,9 +10,12 @@ from ...hands import Node, Asset class UserAssetPermissionMixin(UserPermissionMixin): util = None + def get_cache_policy(self): + return self.request.query_params.get('cache_policy', '0') + @lazyproperty def util(self): - cache_policy = self.request.query_params.get('cache_policy', '0') + cache_policy = self.get_cache_policy() system_user_id = self.request.query_params.get("system_user") util = AssetPermissionUtilV2(self.obj, cache_policy=cache_policy) if system_user_id: diff --git a/jumpserver/jumpserver/apps/perms/forms/__init__.py b/jumpserver/jumpserver/apps/perms/forms/__init__.py index 129901afca3bb3cc75dab4584699261e82cf6821..c6581b858d56218da839da0308d95e68f156ef17 100644 --- a/jumpserver/jumpserver/apps/perms/forms/__init__.py +++ b/jumpserver/jumpserver/apps/perms/forms/__init__.py @@ -3,3 +3,4 @@ from .asset_permission import * from .remote_app_permission import * +from .database_app_permission import * diff --git a/jumpserver/jumpserver/apps/perms/forms/asset_permission.py b/jumpserver/jumpserver/apps/perms/forms/asset_permission.py index 8282f08801262e7a52bc74a473d3328029ad6662..92ca8030f5fd1204e78d98b98ed0860c7aa1f390 100644 --- a/jumpserver/jumpserver/apps/perms/forms/asset_permission.py +++ b/jumpserver/jumpserver/apps/perms/forms/asset_permission.py @@ -5,8 +5,7 @@ from django import forms from django.utils.translation import ugettext_lazy as _ from orgs.mixins.forms import OrgModelForm -from orgs.utils import current_org -from assets.models import Asset, Node +from assets.models import Asset, Node, SystemUser from ..models import AssetPermission, Action __all__ = [ @@ -58,6 +57,12 @@ class AssetPermissionForm(OrgModelForm): nodes_field.queryset = Node.objects.none() users_field.queryset = [] + # 过滤系统用户 + system_users_field = self.fields.get('system_users') + system_users_field.queryset = SystemUser.objects.exclude( + protocol=SystemUser.PROTOCOL_MYSQL + ) + def set_nodes_initial(self, nodes): field = self.fields['nodes'] field.choices = [(n.id, n.full_value) for n in nodes] diff --git a/jumpserver/jumpserver/apps/perms/forms/database_app_permission.py b/jumpserver/jumpserver/apps/perms/forms/database_app_permission.py new file mode 100644 index 0000000000000000000000000000000000000000..57ce4d0e3b96216b54f654114471c0802656d3b1 --- /dev/null +++ b/jumpserver/jumpserver/apps/perms/forms/database_app_permission.py @@ -0,0 +1,49 @@ +# coding: utf-8 +# + +from django.utils.translation import ugettext as _ +from django import forms +from orgs.mixins.forms import OrgModelForm +from assets.models import SystemUser + +from ..models import DatabaseAppPermission + + +__all__ = ['DatabaseAppPermissionCreateUpdateForm'] + + +class DatabaseAppPermissionCreateUpdateForm(OrgModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + users_field = self.fields.get('users') + if self.instance: + users_field.queryset = self.instance.users.all() + else: + users_field.queryset = [] + + # 过滤系统用户 + system_users_field = self.fields.get('system_users') + system_users_field.queryset = SystemUser.objects.filter( + protocol=SystemUser.PROTOCOL_MYSQL + ) + + class Meta: + model = DatabaseAppPermission + exclude = ( + 'id', 'date_created', 'created_by', 'org_id' + ) + widgets = { + 'users': forms.SelectMultiple( + attrs={'class': 'users-select2', 'data-placeholder': _('User')} + ), + 'user_groups': forms.SelectMultiple( + attrs={'class': 'select2', 'data-placeholder': _('User group')} + ), + 'database_apps': forms.SelectMultiple( + attrs={'class': 'select2', 'data-placeholder': _('DatabaseApp')} + ), + 'system_users': forms.SelectMultiple( + attrs={'class': 'select2', 'data-placeholder': _('System users')} + ), + } diff --git a/jumpserver/jumpserver/apps/perms/forms/remote_app_permission.py b/jumpserver/jumpserver/apps/perms/forms/remote_app_permission.py index abac92e05ace9969e4dd41f07a24d5aa35e8c564..c4179297f6fef6458ce75f547a7cd968b83deb8c 100644 --- a/jumpserver/jumpserver/apps/perms/forms/remote_app_permission.py +++ b/jumpserver/jumpserver/apps/perms/forms/remote_app_permission.py @@ -4,7 +4,7 @@ from django.utils.translation import ugettext as _ from django import forms from orgs.mixins.forms import OrgModelForm -from orgs.utils import current_org +from assets.models import SystemUser from ..models import RemoteAppPermission @@ -24,6 +24,12 @@ class RemoteAppPermissionCreateUpdateForm(OrgModelForm): else: users_field.queryset = [] + # 过滤系统用户 + system_users_field = self.fields.get('system_users') + system_users_field.queryset = SystemUser.objects.filter( + protocol=SystemUser.PROTOCOL_RDP + ) + class Meta: model = RemoteAppPermission exclude = ( @@ -43,13 +49,3 @@ class RemoteAppPermissionCreateUpdateForm(OrgModelForm): attrs={'class': 'select2', 'data-placeholder': _('System user')} ) } - - def clean_user_groups(self): - users = self.cleaned_data.get('users') - user_groups = self.cleaned_data.get('user_groups') - - if not users and not user_groups: - raise forms.ValidationError( - _("User or group at least one required") - ) - return self.cleaned_data['user_groups'] diff --git a/jumpserver/jumpserver/apps/perms/migrations/0010_auto_20191218_1705.py b/jumpserver/jumpserver/apps/perms/migrations/0010_auto_20191218_1705.py new file mode 100644 index 0000000000000000000000000000000000000000..c3144bc5dbdfc573f544448e797e8dba72b02c0e --- /dev/null +++ b/jumpserver/jumpserver/apps/perms/migrations/0010_auto_20191218_1705.py @@ -0,0 +1,47 @@ +# Generated by Django 2.1.11 on 2019-12-18 09:05 + +import common.utils.django +from django.conf import settings +from django.db import migrations, models +import django.utils.timezone +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0024_auto_20191118_1612'), + ('assets', '0046_auto_20191218_1705'), + ('applications', '0004_auto_20191218_1705'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('perms', '0009_remoteapppermission_system_users'), + ] + + operations = [ + migrations.CreateModel( + name='DatabaseAppPermission', + fields=[ + ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('is_active', models.BooleanField(default=True, verbose_name='Active')), + ('date_start', models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='Date start')), + ('date_expired', models.DateTimeField(db_index=True, default=common.utils.django.date_expired_default, verbose_name='Date expired')), + ('created_by', models.CharField(blank=True, max_length=128, verbose_name='Created by')), + ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), + ('comment', models.TextField(blank=True, verbose_name='Comment')), + ('database_apps', models.ManyToManyField(blank=True, related_name='granted_by_permissions', to='applications.DatabaseApp', verbose_name='DatabaseApp')), + ('system_users', models.ManyToManyField(related_name='granted_by_database_app_permissions', to='assets.SystemUser', verbose_name='System user')), + ('user_groups', models.ManyToManyField(blank=True, to='users.UserGroup', verbose_name='User group')), + ('users', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='User')), + ], + options={ + 'verbose_name': 'DatabaseApp permission', + 'ordering': ('name',), + }, + ), + migrations.AlterUniqueTogether( + name='databaseapppermission', + unique_together={('org_id', 'name')}, + ), + ] diff --git a/jumpserver/jumpserver/apps/perms/models/__init__.py b/jumpserver/jumpserver/apps/perms/models/__init__.py index 129901afca3bb3cc75dab4584699261e82cf6821..c6581b858d56218da839da0308d95e68f156ef17 100644 --- a/jumpserver/jumpserver/apps/perms/models/__init__.py +++ b/jumpserver/jumpserver/apps/perms/models/__init__.py @@ -3,3 +3,4 @@ from .asset_permission import * from .remote_app_permission import * +from .database_app_permission import * diff --git a/jumpserver/jumpserver/apps/perms/models/base.py b/jumpserver/jumpserver/apps/perms/models/base.py index 878d9d73947a82d2e73a957786bc1174b0882476..950d54b10283615ba712add4c9a554fc98b8f16f 100644 --- a/jumpserver/jumpserver/apps/perms/models/base.py +++ b/jumpserver/jumpserver/apps/perms/models/base.py @@ -80,9 +80,10 @@ class BasePermission(OrgModelMixin): return False def get_all_users(self): - users = set(self.users.all()) - for group in self.user_groups.all(): - _users = group.users.all() - set_or_append_attr_bulk(_users, 'inherit', group.name) - users.update(set(_users)) + from users.models import User + users_id = self.users.all().values_list('id', flat=True) + groups_id = self.user_groups.all().values_list('id', flat=True) + users = User.objects.filter( + Q(id__in=users_id) | Q(groups__id__in=groups_id) + ).distinct() return users diff --git a/jumpserver/jumpserver/apps/perms/models/database_app_permission.py b/jumpserver/jumpserver/apps/perms/models/database_app_permission.py new file mode 100644 index 0000000000000000000000000000000000000000..de2693274cb3797ef9161261964905db87885d71 --- /dev/null +++ b/jumpserver/jumpserver/apps/perms/models/database_app_permission.py @@ -0,0 +1,30 @@ +# coding: utf-8 +# + +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from .base import BasePermission + +__all__ = [ + 'DatabaseAppPermission', +] + + +class DatabaseAppPermission(BasePermission): + database_apps = models.ManyToManyField( + 'applications.DatabaseApp', related_name='granted_by_permissions', + blank=True, verbose_name=_("DatabaseApp") + ) + system_users = models.ManyToManyField( + 'assets.SystemUser', related_name='granted_by_database_app_permissions', + verbose_name=_("System user") + ) + + class Meta: + unique_together = [('org_id', 'name')] + verbose_name = _('DatabaseApp permission') + ordering = ('name',) + + def get_all_database_apps(self): + return self.database_apps.all() diff --git a/jumpserver/jumpserver/apps/perms/serializers/__init__.py b/jumpserver/jumpserver/apps/perms/serializers/__init__.py index 1d099cb33fd143d0c6916f343aad0154019d44fe..7f83bae9becbe27123b012ba640b8ab4843d49f4 100644 --- a/jumpserver/jumpserver/apps/perms/serializers/__init__.py +++ b/jumpserver/jumpserver/apps/perms/serializers/__init__.py @@ -4,3 +4,6 @@ from .asset_permission import * from .user_permission import * from .remote_app_permission import * +from .asset_permission_relation import * +from .database_app_permission import * +from .database_app_permission_relation import * diff --git a/jumpserver/jumpserver/apps/perms/serializers/asset_permission.py b/jumpserver/jumpserver/apps/perms/serializers/asset_permission.py index 94a7dfdbd051ca98c5f410c2d2e88fcf440ffba3..73612a7e6d175592541cbc9db1866d6f76956dff 100644 --- a/jumpserver/jumpserver/apps/perms/serializers/asset_permission.py +++ b/jumpserver/jumpserver/apps/perms/serializers/asset_permission.py @@ -6,12 +6,10 @@ from rest_framework import serializers from common.fields import StringManyToManyField from orgs.mixins.serializers import BulkOrgResourceModelSerializer from perms.models import AssetPermission, Action -from assets.models import Asset __all__ = [ 'AssetPermissionCreateUpdateSerializer', 'AssetPermissionListSerializer', - 'AssetPermissionUpdateUserSerializer', 'AssetPermissionUpdateAssetSerializer', - 'ActionsField', 'AssetPermissionAssetsSerializer', + 'ActionsField', ] @@ -59,23 +57,4 @@ class AssetPermissionListSerializer(BulkOrgResourceModelSerializer): fields = '__all__' -class AssetPermissionUpdateUserSerializer(serializers.ModelSerializer): - class Meta: - model = AssetPermission - fields = ['id', 'users'] - - -class AssetPermissionUpdateAssetSerializer(serializers.ModelSerializer): - - class Meta: - model = AssetPermission - fields = ['id', 'assets'] - - -class AssetPermissionAssetsSerializer(serializers.ModelSerializer): - - class Meta: - model = Asset - only_fields = ['id', 'hostname', 'ip'] - fields = tuple(only_fields) diff --git a/jumpserver/jumpserver/apps/perms/serializers/asset_permission_relation.py b/jumpserver/jumpserver/apps/perms/serializers/asset_permission_relation.py new file mode 100644 index 0000000000000000000000000000000000000000..808f404686951eaf8dec82bd5e1d90cee12f728e --- /dev/null +++ b/jumpserver/jumpserver/apps/perms/serializers/asset_permission_relation.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +# +from rest_framework import serializers + +from common.mixins import BulkSerializerMixin +from common.serializers import AdaptedBulkListSerializer +from assets.models import Asset, Node +from ..models import AssetPermission +from users.models import User + +__all__ = [ + 'AssetPermissionUserRelationSerializer', + 'AssetPermissionUserGroupRelationSerializer', + "AssetPermissionAssetRelationSerializer", + 'AssetPermissionNodeRelationSerializer', + 'AssetPermissionSystemUserRelationSerializer', + 'AssetPermissionAllAssetSerializer', + 'AssetPermissionAllUserSerializer', +] + + +class CurrentAssetPermission(object): + permission = None + + def set_context(self, serializer_field): + self.permission = serializer_field.context['permission'] + + def __call__(self): + return self.permission + + +class RelationMixin(BulkSerializerMixin, serializers.Serializer): + assetpermission_display = serializers.ReadOnlyField() + + def get_field_names(self, declared_fields, info): + fields = super().get_field_names(declared_fields, info) + fields.extend(['assetpermission', "assetpermission_display"]) + return fields + + class Meta: + list_serializer_class = AdaptedBulkListSerializer + + +class AssetPermissionUserRelationSerializer(RelationMixin, serializers.ModelSerializer): + user_display = serializers.ReadOnlyField() + + class Meta(RelationMixin.Meta): + model = AssetPermission.users.through + fields = [ + 'id', 'user', 'user_display', + ] + + +class AssetPermissionAllUserSerializer(serializers.Serializer): + user = serializers.UUIDField(read_only=True, source='id') + user_display = serializers.SerializerMethodField() + + class Meta: + only_fields = ['id', 'username', 'name'] + + @staticmethod + def get_user_display(obj): + return str(obj) + + +class AssetPermissionUserGroupRelationSerializer(RelationMixin, serializers.ModelSerializer): + usergroup_display = serializers.ReadOnlyField() + + class Meta(RelationMixin.Meta): + model = AssetPermission.user_groups.through + fields = [ + 'id', 'usergroup', "usergroup_display", + ] + + +class AssetPermissionAssetRelationSerializer(RelationMixin, serializers.ModelSerializer): + asset_display = serializers.ReadOnlyField() + + class Meta(RelationMixin.Meta): + model = AssetPermission.assets.through + fields = [ + 'id', "asset", "asset_display", + ] + + +class AssetPermissionAllAssetSerializer(serializers.Serializer): + asset = serializers.UUIDField(read_only=True, source='id') + asset_display = serializers.SerializerMethodField() + + class Meta: + only_fields = ['id', 'hostname', 'ip'] + + @staticmethod + def get_asset_display(obj): + return str(obj) + + +class AssetPermissionNodeRelationSerializer(RelationMixin, serializers.ModelSerializer): + node_display = serializers.SerializerMethodField() + + class Meta(RelationMixin.Meta): + model = AssetPermission.nodes.through + fields = [ + 'id', 'node', "node_display", + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.tree = Node.tree() + + def get_node_display(self, obj): + if hasattr(obj, 'node_key'): + return self.tree.get_node_full_tag(obj.node_key) + else: + return obj.node.full_value + + +class AssetPermissionSystemUserRelationSerializer(RelationMixin, serializers.ModelSerializer): + systemuser_display = serializers.ReadOnlyField() + + class Meta(RelationMixin.Meta): + model = AssetPermission.system_users.through + fields = [ + 'id', 'systemuser', 'systemuser_display' + ] diff --git a/jumpserver/jumpserver/apps/perms/serializers/database_app_permission.py b/jumpserver/jumpserver/apps/perms/serializers/database_app_permission.py new file mode 100644 index 0000000000000000000000000000000000000000..a8b8bafcdb336309a3d560ac99af3a3bc7a2ac84 --- /dev/null +++ b/jumpserver/jumpserver/apps/perms/serializers/database_app_permission.py @@ -0,0 +1,39 @@ +# coding: utf-8 +# + +from rest_framework import serializers + +from common.fields import StringManyToManyField +from common.serializers import AdaptedBulkListSerializer +from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from .. import models + +__all__ = [ + 'DatabaseAppPermissionSerializer', 'DatabaseAppPermissionListSerializer' +] + + +class DatabaseAppPermissionSerializer(BulkOrgResourceModelSerializer): + class Meta: + model = models.DatabaseAppPermission + list_serializer_class = AdaptedBulkListSerializer + fields = [ + 'id', 'name', 'users', 'user_groups', + 'database_apps', 'system_users', 'comment', 'is_active', + 'date_start', 'date_expired', 'is_valid', + 'created_by', 'date_created' + ] + read_only_fields = ['created_by', 'date_created'] + + +class DatabaseAppPermissionListSerializer(BulkOrgResourceModelSerializer): + users = StringManyToManyField(many=True, read_only=True) + user_groups = StringManyToManyField(many=True, read_only=True) + database_apps = StringManyToManyField(many=True, read_only=True) + system_users = StringManyToManyField(many=True, read_only=True) + is_valid = serializers.BooleanField() + is_expired = serializers.BooleanField() + + class Meta: + model = models.DatabaseAppPermission + fields = '__all__' diff --git a/jumpserver/jumpserver/apps/perms/serializers/database_app_permission_relation.py b/jumpserver/jumpserver/apps/perms/serializers/database_app_permission_relation.py new file mode 100644 index 0000000000000000000000000000000000000000..1a8263cda89b3a5d7609b88814ac87030bf28788 --- /dev/null +++ b/jumpserver/jumpserver/apps/perms/serializers/database_app_permission_relation.py @@ -0,0 +1,94 @@ +# coding: utf-8 +# +from rest_framework import serializers + +from applications.models import DatabaseApp +from common.mixins import BulkSerializerMixin +from common.serializers import AdaptedBulkListSerializer + +from .. import models + +__all__ = [ + 'DatabaseAppPermissionUserRelationSerializer', + 'DatabaseAppPermissionUserGroupRelationSerializer', + 'DatabaseAppPermissionAllUserSerializer', + 'DatabaseAppPermissionDatabaseAppRelationSerializer', + 'DatabaseAppPermissionAllDatabaseAppSerializer', + 'DatabaseAppPermissionSystemUserRelationSerializer', +] + + +class RelationMixin(BulkSerializerMixin, serializers.Serializer): + databaseapppermission_display = serializers.ReadOnlyField() + + def get_field_names(self, declared_fields, info): + fields = super().get_field_names(declared_fields, info) + fields.extend(['databaseapppermission', "databaseapppermission_display"]) + return fields + + class Meta: + list_serializer_class = AdaptedBulkListSerializer + + +class DatabaseAppPermissionUserRelationSerializer(RelationMixin, serializers.ModelSerializer): + user_display = serializers.ReadOnlyField() + + class Meta(RelationMixin.Meta): + model = models.DatabaseAppPermission.users.through + fields = [ + 'id', 'user', 'user_display', + ] + + +class DatabaseAppPermissionUserGroupRelationSerializer(RelationMixin, serializers.ModelSerializer): + usergroup_display = serializers.ReadOnlyField() + + class Meta(RelationMixin.Meta): + model = models.DatabaseAppPermission.user_groups.through + fields = [ + 'id', 'usergroup', "usergroup_display", + ] + + +class DatabaseAppPermissionAllUserSerializer(serializers.Serializer): + user = serializers.UUIDField(read_only=True, source='id') + user_display = serializers.SerializerMethodField() + + class Meta: + only_fields = ['id', 'username', 'name'] + + @staticmethod + def get_user_display(obj): + return str(obj) + + +class DatabaseAppPermissionDatabaseAppRelationSerializer(RelationMixin, serializers.ModelSerializer): + databaseapp_display = serializers.ReadOnlyField() + + class Meta(RelationMixin.Meta): + model = models.DatabaseAppPermission.database_apps.through + fields = [ + 'id', "databaseapp", "databaseapp_display", + ] + + +class DatabaseAppPermissionAllDatabaseAppSerializer(serializers.Serializer): + databaseapp = serializers.UUIDField(read_only=True, source='id') + databaseapp_display = serializers.SerializerMethodField() + + class Meta: + only_fields = ['id', 'name'] + + @staticmethod + def get_databaseapp_display(obj): + return str(obj) + + +class DatabaseAppPermissionSystemUserRelationSerializer(RelationMixin, serializers.ModelSerializer): + systemuser_display = serializers.ReadOnlyField() + + class Meta(RelationMixin.Meta): + model = models.DatabaseAppPermission.system_users.through + fields = [ + 'id', 'systemuser', 'systemuser_display' + ] diff --git a/jumpserver/jumpserver/apps/perms/serializers/remote_app_permission.py b/jumpserver/jumpserver/apps/perms/serializers/remote_app_permission.py index 4361cff881ed90f5c963b680f2405a51d1794a58..41c5d702274a5bd6d63b900303865e6e70773a3d 100644 --- a/jumpserver/jumpserver/apps/perms/serializers/remote_app_permission.py +++ b/jumpserver/jumpserver/apps/perms/serializers/remote_app_permission.py @@ -3,6 +3,7 @@ from rest_framework import serializers +from common.fields import StringManyToManyField from common.serializers import AdaptedBulkListSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ..models import RemoteAppPermission @@ -12,6 +13,7 @@ __all__ = [ 'RemoteAppPermissionSerializer', 'RemoteAppPermissionUpdateUserSerializer', 'RemoteAppPermissionUpdateRemoteAppSerializer', + 'RemoteAppPermissionListSerializer', ] @@ -27,6 +29,19 @@ class RemoteAppPermissionSerializer(BulkOrgResourceModelSerializer): read_only_fields = ['created_by', 'date_created'] +class RemoteAppPermissionListSerializer(BulkOrgResourceModelSerializer): + users = StringManyToManyField(many=True, read_only=True) + user_groups = StringManyToManyField(many=True, read_only=True) + remote_apps = StringManyToManyField(many=True, read_only=True) + system_users = StringManyToManyField(many=True, read_only=True) + is_valid = serializers.BooleanField() + is_expired = serializers.BooleanField() + + class Meta: + model = RemoteAppPermission + fields = '__all__' + + class RemoteAppPermissionUpdateUserSerializer(serializers.ModelSerializer): class Meta: model = RemoteAppPermission diff --git a/jumpserver/jumpserver/apps/perms/serializers/user_permission.py b/jumpserver/jumpserver/apps/perms/serializers/user_permission.py index d83c15b6f6060afc58c6937a3ed3dc227f62f6b7..c194cf64a6a204e700051fb593aa58ff57cb99c3 100644 --- a/jumpserver/jumpserver/apps/perms/serializers/user_permission.py +++ b/jumpserver/jumpserver/apps/perms/serializers/user_permission.py @@ -13,6 +13,7 @@ __all__ = [ 'AssetGrantedSerializer', 'ActionsSerializer', 'AssetSystemUserSerializer', 'RemoteAppSystemUserSerializer', + 'DatabaseAppSystemUserSerializer', ] @@ -41,11 +42,22 @@ class RemoteAppSystemUserSerializer(serializers.ModelSerializer): read_only_fields = fields +class DatabaseAppSystemUserSerializer(serializers.ModelSerializer): + class Meta: + model = SystemUser + only_fields = ( + 'id', 'name', 'username', 'priority', 'protocol', 'login_mode', + ) + fields = list(only_fields) + read_only_fields = fields + + class AssetGrantedSerializer(serializers.ModelSerializer): """ 被授权资产的数据结构 """ protocols = ProtocolsField(label=_('Protocols'), required=False, read_only=True) + platform = serializers.ReadOnlyField(source='platform_base') class Meta: model = Asset diff --git a/jumpserver/jumpserver/apps/perms/templates/perms/asset_permission_asset.html b/jumpserver/jumpserver/apps/perms/templates/perms/asset_permission_asset.html index f01f7cec80aa6b97a109416272faa539df3a61f6..b6079cdaa34c6141f3ea0d3d72beefb940122b82 100644 --- a/jumpserver/jumpserver/apps/perms/templates/perms/asset_permission_asset.html +++ b/jumpserver/jumpserver/apps/perms/templates/perms/asset_permission_asset.html @@ -2,10 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} {% block content %}
    @@ -54,8 +50,8 @@ - {% trans 'Hostname' %} - {% trans 'IP' %} + {% trans 'Asset' %} + {% trans 'Action' %} @@ -75,7 +71,7 @@ - @@ -112,10 +108,46 @@ {% for node in asset_permission.nodes.all %} - + {{ node.full_value }} - + + + + {% endfor %} + + +
    +
    +
    +
    + {% trans 'System user' %} +
    +
    + + + + + + + + + + + + {% for system_user in object.system_users.all %} + + + {% endfor %} @@ -129,69 +161,83 @@ - {% include 'assets/_asset_list_modal.html' %} {% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/jumpserver/jumpserver/apps/perms/templates/perms/asset_permission_create_update.html b/jumpserver/jumpserver/apps/perms/templates/perms/asset_permission_create_update.html index 5e4c650d08292141b5602af0b36f8e98afd417ca..937e7273edd08a94c48b01777d5e643369e0c858 100644 --- a/jumpserver/jumpserver/apps/perms/templates/perms/asset_permission_create_update.html +++ b/jumpserver/jumpserver/apps/perms/templates/perms/asset_permission_create_update.html @@ -3,9 +3,18 @@ {% load static %} {% load bootstrap3 %} {% block custom_head_css_js %} - - + {% endblock %} {% block content %} @@ -35,20 +44,55 @@ {% endif %} {% csrf_token %} +

    {% trans 'Basic' %}

    {% bootstrap_field form.name layout="horizontal" %} +

    {% trans 'User' %}

    {% bootstrap_field form.users layout="horizontal" %} {% bootstrap_field form.user_groups layout="horizontal" %} +

    {% trans 'Asset' %}

    {% bootstrap_field form.assets layout="horizontal" %} {% bootstrap_field form.nodes layout="horizontal" %} {% bootstrap_field form.system_users layout="horizontal" %} +

    {% trans 'Action' %}

    - {% bootstrap_field form.actions layout="horizontal" %} +
    + +
    +
    +
      +
    • +
      {{ form.actions.0}}
      +
        +
      • +
        {{ form.actions.1}}
        +
      • + +
      • +
        {{ form.actions.4}}
        +
          +
        • +
          {{ form.actions.2}}
          +
        • +
        • +
          {{ form.actions.3}}
          +
        • +
        +
      • +
      + +
    • +
    +
    {{ form.actions.help_text }}
    +
    +
    +
    +

    {% trans 'Other' %}

    @@ -100,18 +144,23 @@ {% endblock %} diff --git a/jumpserver/jumpserver/apps/perms/templates/perms/asset_permission_detail.html b/jumpserver/jumpserver/apps/perms/templates/perms/asset_permission_detail.html index 044438c43e2f4943b3e0d26233d59322f92ba44b..6930cfc5abdc28d1acad5f6a953f211a47319b7a 100644 --- a/jumpserver/jumpserver/apps/perms/templates/perms/asset_permission_detail.html +++ b/jumpserver/jumpserver/apps/perms/templates/perms/asset_permission_detail.html @@ -2,11 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} - {% block content %}
    @@ -135,42 +130,7 @@
    -
    -
    - {% trans 'System user' %} -
    -
    -
    + +
    + +
    {{ system_user|truncatechars:21}} +
    - - - - - - - - - - {% for system_user in object.system_users.all %} - - - - - {% endfor %} - -
    - -
    - -
    {{ system_user }} - -
    -
    -
    @@ -182,17 +142,7 @@ @@ -14,66 +12,37 @@ .toggle { cursor: pointer; } - .detail-key { - width: 70px; - } {% endblock %} -{% block content %} -
    -
    -
    - {% include 'assets/_node_tree.html' %} -
    -
    -
    -
    - -
    -
    -
    -
    - - - -
    - - - - - - - - - - - - - - - - -
    {% trans 'Name' %}{% trans 'User' %}{% trans 'User group' %}{% trans 'Asset' %}{% trans 'Node'%}{% trans 'System user' %}{% trans 'Validity' %}{% trans 'Action' %}
    -
    -
    -
    -
    - - +{% block table_container %} +
    + + + +
    + + + + + + + + + + + + + + + + +
    {% trans 'Name' %}{% trans 'User' %}{% trans 'User group' %}{% trans 'Asset' %}{% trans 'Node'%}{% trans 'System user' %}{% trans 'Validity' %}{% trans 'Action' %}
    +{% include '_filter_dropdown.html' %} {% endblock %} {% block custom_foot_js %} @@ -105,9 +74,6 @@ function beforeNodeAsync(treeId, treeNode) { return true } -function makeLabel(data) { - return "" + data[1] + "
    " -} function format(d) { var data = ""; @@ -185,12 +151,12 @@ function initTable() { $(td).html(update_btn + del_btn); }} ], - ajax_url: '{% url "api-perms:asset-permission-list" %}?display=1', + ajax_url: '{% url "api-perms:asset-permission-list" %}', columns: [ {data: "id"}, {data: "name"}, {data: "users", orderable: false}, {data: "user_groups", orderable: false}, {data: "assets", orderable: false}, {data: "nodes", orderable: false}, {data: "system_users", orderable: false}, - {data: "is_valid", orderable: false}, {data: "id", orderable: false, width: "100px"} + {data: "is_valid", orderable: false}, {data: "id", orderable: false, width: "120px"} ], select: {}, op_html: $('#actions').html() @@ -209,24 +175,25 @@ function initTree() { }) } -function toggle() { - if (show === 0) { - $("#split-left").hide(500, function () { - $("#split-right").attr("class", "col-lg-12"); - $("#toggle-icon").attr("class", "fa fa-angle-right fa-x"); - show = 1; - }); - } else { - $("#split-right").attr("class", "col-lg-9"); - $("#toggle-icon").attr("class", "fa fa-angle-left fa-x"); - $("#split-left").show(500); - show = 0; - } -} $(document).ready(function(){ initTable(); initTree(); + var filterMenu = [ + {title: "{% trans 'Name' %}", value: "name"}, + {title: "{% trans 'Validity' %}", value: "is_valid"}, + {title: "{% trans 'Username' %}", value: "username"}, + {title: "{% trans 'User group' %}", value: "user_group"}, + {title: "{% trans 'IP' %}", value: "ip"}, + {title: "{% trans 'Hostname' %}", value: "hostname"}, + {title: "{% trans 'Node' %}", value: "node"}, + {title: "{% trans 'System user' %}", value: "system_user"}, + {title: "{% trans 'Inherit' %}", value: "all", submenu: [ + {title: "{% trans 'Include' %}", value: "1"}, + {title: "{% trans 'Exclude' %}", value: "0"}, + ]}, + ]; + initTableFilterDropdown('#permission_list_table_filter input', filterMenu) }) .on('click', '.btn-del', function () { var $this = $(this); @@ -284,27 +251,8 @@ $(document).ready(function(){ detailRows.push(tr.attr('id')); } } -}).on('click', '#permission_list_table_filter input', function (e) { - e.preventDefault(); - e.stopPropagation(); - var position = $('#permission_list_table_filter input').offset(); - var y = position['top']; - var x = position['left']; - x -= 220; - y += 30; - - $('.search-help').css({"top":y+"px", "left":x+"px", "position": "absolute"}); - $('.dropdown-menu.search-help').show(); -}).on('click', '.search-item', function (e) { - e.preventDefault(); - e.stopPropagation(); - var value = $(this).data('value'); - var old_value = $('#permission_list_table_filter input').val(); - var new_value = old_value + ' ' + value + ':'; - $('#permission_list_table_filter input').val(new_value.trim()); - $('.dropdown-menu.search-help').hide(); - $('#permission_list_table_filter input').focus() -}).on('click', 'body', function (e) { +}) +.on('click', 'body', function (e) { $('.dropdown-menu.search-help').hide() }) diff --git a/jumpserver/jumpserver/apps/perms/templates/perms/asset_permission_user.html b/jumpserver/jumpserver/apps/perms/templates/perms/asset_permission_user.html index f06c3409c14669a649d14d5f79f944ee4650dba7..bb9ca375a2085bdc256882444d0f92b8e3b82525 100644 --- a/jumpserver/jumpserver/apps/perms/templates/perms/asset_permission_user.html +++ b/jumpserver/jumpserver/apps/perms/templates/perms/asset_permission_user.html @@ -2,10 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} {% block content %}
    @@ -48,29 +44,19 @@
    - +
    + - - + - {% for user in object_list %} - - - - - - {% endfor %}
    + + {% trans 'Name' %}{% trans 'Username' %}{% trans 'Action' %}
    {{ user.name }}{{ user.username }} - -
    -
    - {% include '_pagination.html' %} -
    @@ -85,10 +71,7 @@
    - @@ -113,7 +96,7 @@ - {% for user_group in user_groups_remain %} {% endfor %} @@ -128,7 +111,7 @@
    {% for user_group in asset_permission.user_groups.all %} - + {{ user_group }} @@ -145,120 +128,124 @@
    - {% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/jumpserver/jumpserver/apps/perms/templates/perms/database_app_permission_create_update.html b/jumpserver/jumpserver/apps/perms/templates/perms/database_app_permission_create_update.html new file mode 100644 index 0000000000000000000000000000000000000000..a779e5826d368bc49318ab8e289f18d1ee8781bb --- /dev/null +++ b/jumpserver/jumpserver/apps/perms/templates/perms/database_app_permission_create_update.html @@ -0,0 +1,143 @@ +{% extends 'base.html' %} +{% load i18n %} +{% load static %} +{% load bootstrap3 %} +{% block custom_head_css_js %} + +{% endblock %} + +{% block content %} +
    +
    +
    +
    +
    +
    {{ action }}
    + +
    +
    +
    + {% if form.non_field_errors %} +
    + {{ form.non_field_errors }} +
    + {% endif %} + {% csrf_token %} + +

    {% trans 'Basic' %}

    + {% bootstrap_field form.name layout="horizontal" %} +
    + +

    {% trans 'User' %}

    + {% bootstrap_field form.users layout="horizontal" %} + {% bootstrap_field form.user_groups layout="horizontal" %} +
    + +

    {% trans 'DatabaseApp' %}

    + {% bootstrap_field form.database_apps layout="horizontal" %} + {% bootstrap_field form.system_users layout="horizontal" %} +
    + +

    {% trans 'Other' %}

    +
    + +
    + {{ form.is_active }} +
    +
    +
    + +
    +
    + + {% if form.errors %} + + to + + {% else %} + + to + + {% endif %} +
    + {{ form.date_expired.errors }} + {{ form.date_start.errors }} +
    +
    + + {% bootstrap_field form.comment layout="horizontal" %} + +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    +
    +{% endblock %} +{% block custom_foot_js %} + + + + + + +{% endblock %} diff --git a/jumpserver/jumpserver/apps/perms/templates/perms/database_app_permission_database_app.html b/jumpserver/jumpserver/apps/perms/templates/perms/database_app_permission_database_app.html new file mode 100644 index 0000000000000000000000000000000000000000..0a23618d00c38a679bb1514a15622a598dc8e5fb --- /dev/null +++ b/jumpserver/jumpserver/apps/perms/templates/perms/database_app_permission_database_app.html @@ -0,0 +1,237 @@ +{% extends 'base.html' %} +{% load static %} +{% load i18n %} + +{% block content %} +
    +
    +
    +
    + +
    +
    +
    +
    + {% trans 'DatabaseApp list of ' %} {{ database_app_permission.name }} +
    + + + + + + + + + + +
    +
    +
    + + + + + + + + + + +
    + + {% trans 'DatabaseApp' %}{% trans 'Action' %}
    +
    +
    +
    +
    +
    +
    + {% trans 'Add DatabaseApp to this permission' %} +
    +
    + + + + + + + + + + + +
    + +
    + +
    +
    +
    + +
    +
    + {% trans 'System user' %} +
    +
    + + + + + + + + + + + + {% for system_user in object.system_users.all %} + + + + + {% endfor %} + +
    + +
    + +
    {{ system_user|truncatechars:21}} + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/jumpserver/jumpserver/apps/perms/templates/perms/database_app_permission_detail.html b/jumpserver/jumpserver/apps/perms/templates/perms/database_app_permission_detail.html new file mode 100644 index 0000000000000000000000000000000000000000..feae4db248c23e7e97bc47374676ef9d6895aa3d --- /dev/null +++ b/jumpserver/jumpserver/apps/perms/templates/perms/database_app_permission_detail.html @@ -0,0 +1,157 @@ +{% extends 'base.html' %} +{% load static %} +{% load i18n %} + +{% block content %} +
    +
    +
    +
    + +
    +
    +
    +
    + {{ object.name }} +
    + + + + + + + + + + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {% trans 'Name' %}:{{ object.name }}
    {% trans 'User count' %}:{{ object.users.count }}
    {% trans 'User group count' %}:{{ object.user_groups.count }}
    {% trans 'DatabaseApp count' %}:{{ object.database_apps.count }}
    {% trans 'System user count' %}:{{ object.system_users.count }}
    {% trans 'Date start' %}:{{ object.date_start }}
    {% trans 'Date expired' %}:{{ object.date_expired }}
    {% trans 'Date created' %}:{{ object.date_created }}
    {% trans 'Created by' %}:{{ object.created_by }}
    {% trans 'Comment' %}:{{ object.comment }}
    +
    +
    +
    + +
    +
    +
    + {% trans 'Quick update' %} +
    +
    + + + + + + + +
    {% trans 'Active' %} : +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/jumpserver/jumpserver/apps/perms/templates/perms/database_app_permission_list.html b/jumpserver/jumpserver/apps/perms/templates/perms/database_app_permission_list.html new file mode 100644 index 0000000000000000000000000000000000000000..b85454cb8ea42faaa9edeba192d98d1ba8569e12 --- /dev/null +++ b/jumpserver/jumpserver/apps/perms/templates/perms/database_app_permission_list.html @@ -0,0 +1,99 @@ +{% extends '_base_list.html' %} +{% load i18n static %} +{% block table_search %}{% endblock %} +{% block table_container %} + + + + + + + + + + + + + + + + +
    + + {% trans 'Name' %}{% trans 'User' %}{% trans 'User group' %}{% trans 'DatabaseApp' %}{% trans 'System user' %}{% trans 'Validity' %}{% trans 'Action' %}
    +{% endblock %} +{% block content_bottom_left %}{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/jumpserver/jumpserver/apps/perms/templates/perms/database_app_permission_user.html b/jumpserver/jumpserver/apps/perms/templates/perms/database_app_permission_user.html new file mode 100644 index 0000000000000000000000000000000000000000..603f60a6e939fc358715ff57a5adc1b02ad92dbb --- /dev/null +++ b/jumpserver/jumpserver/apps/perms/templates/perms/database_app_permission_user.html @@ -0,0 +1,251 @@ +{% extends 'base.html' %} +{% load static %} +{% load i18n %} + +{% block content %} +
    +
    +
    +
    + +
    +
    +
    +
    + {% trans 'User list of ' %} {{ database_app_permission.name }} +
    + + + + + + + + + + +
    +
    +
    + + + + + + + + + + +
    + + {% trans 'Name' %}{% trans 'Action' %}
    +
    +
    +
    +
    +
    +
    + {% trans 'Add user to permission' %} +
    +
    + + + + + + + + + + + +
    + +
    + +
    +
    +
    + +
    +
    + {% trans 'Add user group to permission' %} +
    +
    + + + + + + + + + + + + {% for user_group in database_app_permission.user_groups.all %} + + + + + {% endfor %} + +
    + +
    + +
    {{ user_group }} + +
    +
    +
    +
    +
    +
    +
    +
    +
    +{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/jumpserver/jumpserver/apps/perms/templates/perms/remote_app_permission_create_update.html b/jumpserver/jumpserver/apps/perms/templates/perms/remote_app_permission_create_update.html index e7e9eb09913f59fb79022d6f5d4bf73783379682..f6e0cda7d078cafb804f0b5ac02ffa1fbdaf0dff 100644 --- a/jumpserver/jumpserver/apps/perms/templates/perms/remote_app_permission_create_update.html +++ b/jumpserver/jumpserver/apps/perms/templates/perms/remote_app_permission_create_update.html @@ -3,8 +3,6 @@ {% load static %} {% load bootstrap3 %} {% block custom_head_css_js %} - - {% endblock %} @@ -115,8 +113,8 @@ $(document).ready(function () { closeOnSelect: false }); usersSelect2Init('.users-select2'); - $('#date_start').daterangepicker(dateOptions); - $('#date_expired').daterangepicker(dateOptions); + initDateRangePicker('#date_start'); + initDateRangePicker('#date_expired'); }) .on("submit", "form", function (evt) { evt.preventDefault(); @@ -125,7 +123,7 @@ $(document).ready(function () { var method = "POST"; var the_url = '{% url "api-perms:remote-app-permission-list" %}'; var redirect_to = '{% url "perms:remote-app-permission-list" %}'; - {% if type == "update" %} + {% if api_action == "update" %} the_url = '{% url "api-perms:remote-app-permission-detail" pk=object.id %}'; method = "PUT"; {% endif %} diff --git a/jumpserver/jumpserver/apps/perms/templates/perms/remote_app_permission_detail.html b/jumpserver/jumpserver/apps/perms/templates/perms/remote_app_permission_detail.html index f2ec8fa35e8d0478f3f24f9f0d3feace2ca28f87..aedef832e3a1775e555c12613f4647aa4aeefab7 100644 --- a/jumpserver/jumpserver/apps/perms/templates/perms/remote_app_permission_detail.html +++ b/jumpserver/jumpserver/apps/perms/templates/perms/remote_app_permission_detail.html @@ -2,11 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} - {% block content %}
    @@ -244,4 +239,4 @@ $(document).ready(function () { }); }) -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/jumpserver/jumpserver/apps/perms/templates/perms/remote_app_permission_list.html b/jumpserver/jumpserver/apps/perms/templates/perms/remote_app_permission_list.html index 3812b747bd2a47380a305c85b7501c73778831f7..f6071430ab5d009b88bfd9670632fa2c86e2130b 100644 --- a/jumpserver/jumpserver/apps/perms/templates/perms/remote_app_permission_list.html +++ b/jumpserver/jumpserver/apps/perms/templates/perms/remote_app_permission_list.html @@ -75,7 +75,7 @@ function initTable() { {data: "remote_apps", orderable: false}, {data: "system_users", orderable: false}, {data: "is_valid", orderable: false}, - {data: "id", orderable: false} + {data: "id", orderable: false, width: "120px"} ], op_html: $('#actions').html() }; diff --git a/jumpserver/jumpserver/apps/perms/templates/perms/remote_app_permission_remote_app.html b/jumpserver/jumpserver/apps/perms/templates/perms/remote_app_permission_remote_app.html index 0916133175c9dd7e9e63a119f02c5c221b098806..9914890332e7873d834ea3d701b1443b62b8bb1f 100644 --- a/jumpserver/jumpserver/apps/perms/templates/perms/remote_app_permission_remote_app.html +++ b/jumpserver/jumpserver/apps/perms/templates/perms/remote_app_permission_remote_app.html @@ -2,10 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} {% block content %}
    @@ -161,4 +157,4 @@ removeRemoteApps(remote_apps) }) -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/jumpserver/jumpserver/apps/perms/templates/perms/remote_app_permission_user.html b/jumpserver/jumpserver/apps/perms/templates/perms/remote_app_permission_user.html index e18e4f694fafefee1e70512fb7014e83ee294a3d..d222dfb7c4a12615a07885ebdd74a62b99fd7ee4 100644 --- a/jumpserver/jumpserver/apps/perms/templates/perms/remote_app_permission_user.html +++ b/jumpserver/jumpserver/apps/perms/templates/perms/remote_app_permission_user.html @@ -2,10 +2,6 @@ {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} {% block content %}
    @@ -253,4 +249,4 @@ $tr.remove() }) -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/jumpserver/jumpserver/apps/perms/urls/api_urls.py b/jumpserver/jumpserver/apps/perms/urls/api_urls.py index 5ffa0d14f8804120849bc74984bc4446b7c518bf..9ec3b754f31f4fb07c34e83406334e69b2ea3b59 100644 --- a/jumpserver/jumpserver/apps/perms/urls/api_urls.py +++ b/jumpserver/jumpserver/apps/perms/urls/api_urls.py @@ -1,115 +1,20 @@ # coding:utf-8 -from django.urls import path, re_path -from rest_framework import routers +from django.urls import re_path from common import api as capi -from .. import api +from .asset_permission import asset_permission_urlpatterns +from .remote_app_permission import remote_app_permission_urlpatterns +from .database_app_permission import database_app_permission_urlpatterns app_name = 'perms' -router = routers.DefaultRouter() -router.register('asset-permissions', api.AssetPermissionViewSet, 'asset-permission') -router.register('remote-app-permissions', api.RemoteAppPermissionViewSet, 'remote-app-permission') - - -asset_permission_urlpatterns = [ - # Assets - path('users//assets/', api.UserGrantedAssetsApi.as_view(), name='user-assets'), - path('users/assets/', api.UserGrantedAssetsApi.as_view(), name='my-assets'), - - # Assets as tree - path('users//assets/tree/', api.UserGrantedAssetsAsTreeApi.as_view(), name='user-assets-as-tree'), - path('users/assets/tree/', api.UserGrantedAssetsAsTreeApi.as_view(), name='my-assets-as-tree'), - - # Nodes - path('users//nodes/', api.UserGrantedNodesApi.as_view(), name='user-nodes'), - path('users/nodes/', api.UserGrantedNodesApi.as_view(), name='my-nodes'), - - # Node children - path('users//nodes/children/', api.UserGrantedNodesApi.as_view(), name='user-nodes-children'), - path('users/nodes/children/', api.UserGrantedNodesApi.as_view(), name='my-nodes-children'), - - # Node as tree - path('users//nodes/tree/', api.UserGrantedNodesAsTreeApi.as_view(), name='user-nodes-as-tree'), - path('users/nodes/tree/', api.UserGrantedNodesAsTreeApi.as_view(), name='my-nodes-as-tree'), - - # Node with assets as tree - path('users//nodes-with-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='user-nodes-with-assets-as-tree'), - path('users/nodes-with-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='my-nodes-with-assets-as-tree'), - - # Node children as tree - path('users//nodes/children/tree/', api.UserGrantedNodeChildrenAsTreeApi.as_view(), name='user-nodes-children-as-tree'), - path('users/nodes/children/tree/', api.UserGrantedNodeChildrenAsTreeApi.as_view(), name='my-nodes-children-as-tree'), - - # Node children with assets as tree - path('users//nodes/children-with-assets/tree/', api.UserGrantedNodeChildrenWithAssetsAsTreeApi.as_view(), name='user-nodes-children-with-assets-as-tree'), - path('users/nodes/children-with-assets/tree/', api.UserGrantedNodeChildrenWithAssetsAsTreeApi.as_view(), name='my-nodes-children-with-assets-as-tree'), - - # Node assets - path('users//nodes//assets/', api.UserGrantedNodeAssetsApi.as_view(), name='user-node-assets'), - path('users/nodes//assets/', api.UserGrantedNodeAssetsApi.as_view(), name='my-node-assets'), - - # Asset System users - path('users//assets//system-users/', api.UserGrantedAssetSystemUsersApi.as_view(), name='user-asset-system-users'), - path('users/assets//system-users/', api.UserGrantedAssetSystemUsersApi.as_view(), name='my-asset-system-users'), - - # 查询某个用户组授权的资产和资产组 - path('user-groups//assets/', api.UserGroupGrantedAssetsApi.as_view(), name='user-group-assets'), - path('user-groups//nodes/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes'), - path('user-groups//nodes/children/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes-children'), - path('user-groups//nodes/children/tree/', api.UserGroupGrantedNodeChildrenAsTreeApi.as_view(), name='user-group-nodes-children-as-tree'), - path('user-groups//nodes//assets/', api.UserGroupGrantedNodeAssetsApi.as_view(), name='user-group-node-assets'), - path('user-groups//assets//system-users/', api.UserGroupGrantedAssetSystemUsersApi.as_view(), name='user-group-asset-system-users'), - - # 用户和资产授权变更 - path('asset-permissions//users/remove/', api.AssetPermissionRemoveUserApi.as_view(), name='asset-permission-remove-user'), - path('asset-permissions//users/add/', api.AssetPermissionAddUserApi.as_view(), name='asset-permission-add-user'), - path('asset-permissions//assets/remove/', api.AssetPermissionRemoveAssetApi.as_view(), name='asset-permission-remove-asset'), - path('asset-permissions//assets/add/', api.AssetPermissionAddAssetApi.as_view(), name='asset-permission-add-asset'), - - # 授权规则中授权的资产 - path('asset-permissions//assets/', api.AssetPermissionAssetsApi.as_view(), name='asset-permission-assets'), - - # 验证用户是否有某个资产和系统用户的权限 - path('asset-permissions/user/validate/', api.ValidateUserAssetPermissionApi.as_view(), name='validate-user-asset-permission'), - path('asset-permissions/user/actions/', api.GetUserAssetPermissionActionsApi.as_view(), name='get-user-asset-permission-actions'), - - # 刷新缓存 - path('asset-permissions/cache/refresh/', api.RefreshAssetPermissionCacheApi.as_view(), name='refresh-asset-permission-cache'), -] - - -remote_app_permission_urlpatterns = [ - # 查询用户授权的RemoteApp - path('users//remote-apps/', api.UserGrantedRemoteAppsApi.as_view(), name='user-remote-apps'), - path('users/remote-apps/', api.UserGrantedRemoteAppsApi.as_view(), name='my-remote-apps'), - - # 获取用户授权的RemoteApp树 - path('users//remote-apps/tree/', api.UserGrantedRemoteAppsAsTreeApi.as_view(), name='user-remote-apps-as-tree'), - path('users/remote-apps/tree/', api.UserGrantedRemoteAppsAsTreeApi.as_view(), name='my-remote-apps-as-tree'), - - # 查询用户组授权的RemoteApp - path('user-groups//remote-apps/', api.UserGroupGrantedRemoteAppsApi.as_view(), name='user-group-remote-apps'), - - # RemoteApp System users - path('users//remote-apps//system-users/', api.UserGrantedRemoteAppSystemUsersApi.as_view(), name='user-remote-app-system-users'), - path('users/remote-apps//system-users/', api.UserGrantedRemoteAppSystemUsersApi.as_view(), name='my-remote-app-system-users'), - - # 校验用户对RemoteApp的权限 - path('remote-app-permissions/user/validate/', api.ValidateUserRemoteAppPermissionApi.as_view(), name='validate-user-remote-app-permission'), - - # 用户和RemoteApp变更 - path('remote-app-permissions//users/add/', api.RemoteAppPermissionAddUserApi.as_view(), name='remote-app-permission-add-user'), - path('remote-app-permissions//users/remove/', api.RemoteAppPermissionRemoveUserApi.as_view(), name='remote-app-permission-remove-user'), - path('remote-app-permissions//remote-apps/remove/', api.RemoteAppPermissionRemoveRemoteAppApi.as_view(), name='remote-app-permission-remove-remote-app'), - path('remote-app-permissions//remote-apps/add/', api.RemoteAppPermissionAddRemoteAppApi.as_view(), name='remote-app-permission-add-remote-app'), -] old_version_urlpatterns = [ re_path('(?Puser|user-group|asset-permission|remote-app-permission)/.*', capi.redirect_plural_name_api) ] -urlpatterns = asset_permission_urlpatterns + remote_app_permission_urlpatterns + old_version_urlpatterns - -urlpatterns += router.urls +urlpatterns = asset_permission_urlpatterns + \ + remote_app_permission_urlpatterns + \ + database_app_permission_urlpatterns + \ + old_version_urlpatterns diff --git a/jumpserver/jumpserver/apps/perms/urls/asset_permission.py b/jumpserver/jumpserver/apps/perms/urls/asset_permission.py new file mode 100644 index 0000000000000000000000000000000000000000..4a649c9920bdb5f3a8b994e106e8679d8f5f81c1 --- /dev/null +++ b/jumpserver/jumpserver/apps/perms/urls/asset_permission.py @@ -0,0 +1,86 @@ +# coding:utf-8 + +from django.urls import path, include +from rest_framework_bulk.routes import BulkRouter +from .. import api + +router = BulkRouter() +router.register('asset-permissions', api.AssetPermissionViewSet, 'asset-permission') +router.register('asset-permissions-users-relations', api.AssetPermissionUserRelationViewSet, 'asset-permissions-users-relation') +router.register('asset-permissions-user-groups-relations', api.AssetPermissionUserGroupRelationViewSet, 'asset-permissions-user-groups-relation') +router.register('asset-permissions-assets-relations', api.AssetPermissionAssetRelationViewSet, 'asset-permissions-assets-relation') +router.register('asset-permissions-nodes-relations', api.AssetPermissionNodeRelationViewSet, 'asset-permissions-nodes-relation') +router.register('asset-permissions-system-users-relations', api.AssetPermissionSystemUserRelationViewSet, 'asset-permissions-system-users-relation') + +user_permission_urlpatterns = [ + path('/assets/', api.UserGrantedAssetsApi.as_view(), name='user-assets'), + path('assets/', api.UserGrantedAssetsApi.as_view(), name='my-assets'), + + # Assets as tree + path('/assets/tree/', api.UserGrantedAssetsAsTreeApi.as_view(), name='user-assets-as-tree'), + path('assets/tree/', api.UserGrantedAssetsAsTreeApi.as_view(), name='my-assets-as-tree'), + + # Nodes + path('/nodes/', api.UserGrantedNodesApi.as_view(), name='user-nodes'), + path('nodes/', api.UserGrantedNodesApi.as_view(), name='my-nodes'), + + # Node children + path('/nodes/children/', api.UserGrantedNodesApi.as_view(), name='user-nodes-children'), + path('nodes/children/', api.UserGrantedNodesApi.as_view(), name='my-nodes-children'), + + # Node as tree + path('/nodes/tree/', api.UserGrantedNodesAsTreeApi.as_view(), name='user-nodes-as-tree'), + path('nodes/tree/', api.UserGrantedNodesAsTreeApi.as_view(), name='my-nodes-as-tree'), + + # Node with assets as tree + path('/nodes-with-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='user-nodes-with-assets-as-tree'), + path('nodes-with-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='my-nodes-with-assets-as-tree'), + + # Node children as tree + path('/nodes/children/tree/', api.UserGrantedNodeChildrenAsTreeApi.as_view(), name='user-nodes-children-as-tree'), + path('nodes/children/tree/', api.UserGrantedNodeChildrenAsTreeApi.as_view(), name='my-nodes-children-as-tree'), + + # Node children with assets as tree + path('/nodes/children-with-assets/tree/', api.UserGrantedNodeChildrenWithAssetsAsTreeApi.as_view(), name='user-nodes-children-with-assets-as-tree'), + path('nodes/children-with-assets/tree/', api.UserGrantedNodeChildrenWithAssetsAsTreeApi.as_view(), name='my-nodes-children-with-assets-as-tree'), + + # Node assets + path('/nodes//assets/', api.UserGrantedNodeAssetsApi.as_view(), name='user-node-assets'), + path('nodes//assets/', api.UserGrantedNodeAssetsApi.as_view(), name='my-node-assets'), + + # Asset System users + path('/assets//system-users/', api.UserGrantedAssetSystemUsersApi.as_view(), name='user-asset-system-users'), + path('assets//system-users/', api.UserGrantedAssetSystemUsersApi.as_view(), name='my-asset-system-users'), +] + +user_group_permission_urlpatterns = [ + # 查询某个用户组授权的资产和资产组 + path('/assets/', api.UserGroupGrantedAssetsApi.as_view(), name='user-group-assets'), + path('/nodes/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes'), + path('/nodes/children/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes-children'), + path('/nodes/children/tree/', api.UserGroupGrantedNodeChildrenAsTreeApi.as_view(), name='user-group-nodes-children-as-tree'), + path('/nodes//assets/', api.UserGroupGrantedNodeAssetsApi.as_view(), name='user-group-node-assets'), + path('/assets//system-users/', api.UserGroupGrantedAssetSystemUsersApi.as_view(), name='user-group-asset-system-users'), +] + +permission_urlpatterns = [ + # 授权规则中授权的资产 + path('/assets/all/', api.AssetPermissionAllAssetListApi.as_view(), name='asset-permission-all-assets'), + path('/users/all/', api.AssetPermissionAllUserListApi.as_view(), name='asset-permission-all-users'), + + # 验证用户是否有某个资产和系统用户的权限 + path('user/validate/', api.ValidateUserAssetPermissionApi.as_view(), name='validate-user-asset-permission'), + path('user/actions/', api.GetUserAssetPermissionActionsApi.as_view(), name='get-user-asset-permission-actions'), + + # 刷新缓存 + path('cache/refresh/', api.RefreshAssetPermissionCacheApi.as_view(), name='refresh-asset-permission-cache'), +] + +asset_permission_urlpatterns = [ + # Assets + path('users/', include(user_permission_urlpatterns)), + path('user-groups/', include(user_group_permission_urlpatterns)), + path('asset-permissions/', include(permission_urlpatterns)), +] + +asset_permission_urlpatterns += router.urls diff --git a/jumpserver/jumpserver/apps/perms/urls/database_app_permission.py b/jumpserver/jumpserver/apps/perms/urls/database_app_permission.py new file mode 100644 index 0000000000000000000000000000000000000000..a793f980e8d1cacf10034b021ae81e068b37be7a --- /dev/null +++ b/jumpserver/jumpserver/apps/perms/urls/database_app_permission.py @@ -0,0 +1,47 @@ +# coding: utf-8 +# + +from django.urls import path, include +from rest_framework_bulk.routes import BulkRouter +from .. import api + + +router = BulkRouter() +router.register('database-app-permissions', api.DatabaseAppPermissionViewSet, 'database-app-permission') +router.register('database-app-permissions-users-relations', api.DatabaseAppPermissionUserRelationViewSet, 'database-app-permissions-users-relation') +router.register('database-app-permissions-user-groups-relations', api.DatabaseAppPermissionUserGroupRelationViewSet, 'database-app-permissions-user-groups-relation') +router.register('database-app-permissions-database-apps-relations', api.DatabaseAppPermissionDatabaseAppRelationViewSet, 'database-app-permissions-database-apps-relation') +router.register('database-app-permissions-system-users-relations', api.DatabaseAppPermissionSystemUserRelationViewSet, 'database-app-permissions-system-users-relation') + +user_permission_urlpatterns = [ + path('/database-apps/', api.UserGrantedDatabaseAppsApi.as_view(), name='user-database-apps'), + path('database-apps/', api.UserGrantedDatabaseAppsApi.as_view(), name='my-database-apps'), + + # DatabaseApps as tree + path('/database-apps/tree/', api.UserGrantedDatabaseAppsAsTreeApi.as_view(), name='user-databases-apps-tree'), + path('database-apps/tree/', api.UserGrantedDatabaseAppsAsTreeApi.as_view(), name='my-databases-apps-tree'), + + path('/database-apps//system-users/', api.UserGrantedDatabaseAppSystemUsersApi.as_view(), name='user-database-app-system-users'), + path('database-apps//system-users/', api.UserGrantedDatabaseAppSystemUsersApi.as_view(), name='user-database-app-system-users'), +] + +user_group_permission_urlpatterns = [ + path('/database-apps/', api.UserGroupGrantedDatabaseAppsApi.as_view(), name='user-group-database-apps'), +] + +permission_urlpatterns = [ + # 授权规则中授权的用户和数据库应用 + path('/users/all/', api.DatabaseAppPermissionAllUserListApi.as_view(), name='database-app-permission-all-users'), + path('/database-apps/all/', api.DatabaseAppPermissionAllDatabaseAppListApi.as_view(), name='database-app-permission-all-database-apps'), + + # 验证用户是否有某个数据库应用的权限 + path('user/validate/', api.ValidateUserDatabaseAppPermissionApi.as_view(), name='validate-user-database-app-permission'), +] + +database_app_permission_urlpatterns = [ + path('users/', include(user_permission_urlpatterns)), + path('user-groups/', include(user_group_permission_urlpatterns)), + path('database-app-permissions/', include(permission_urlpatterns)) +] + +database_app_permission_urlpatterns += router.urls diff --git a/jumpserver/jumpserver/apps/perms/urls/remote_app_permission.py b/jumpserver/jumpserver/apps/perms/urls/remote_app_permission.py new file mode 100644 index 0000000000000000000000000000000000000000..8f83d72d0499aeb7aa371ac64a5d60cc23c38cbf --- /dev/null +++ b/jumpserver/jumpserver/apps/perms/urls/remote_app_permission.py @@ -0,0 +1,38 @@ +# coding:utf-8 + +from django.urls import path +from rest_framework_bulk.routes import BulkRouter +from .. import api + + +router = BulkRouter() +router.register('remote-app-permissions', api.RemoteAppPermissionViewSet, 'remote-app-permission') + +remote_app_permission_urlpatterns = [ + # 查询用户授权的RemoteApp + path('users//remote-apps/', api.UserGrantedRemoteAppsApi.as_view(), name='user-remote-apps'), + path('users/remote-apps/', api.UserGrantedRemoteAppsApi.as_view(), name='my-remote-apps'), + + # 获取用户授权的RemoteApp树 + path('users//remote-apps/tree/', api.UserGrantedRemoteAppsAsTreeApi.as_view(), name='user-remote-apps-as-tree'), + path('users/remote-apps/tree/', api.UserGrantedRemoteAppsAsTreeApi.as_view(), name='my-remote-apps-as-tree'), + + # 查询用户组授权的RemoteApp + path('user-groups//remote-apps/', api.UserGroupGrantedRemoteAppsApi.as_view(), name='user-group-remote-apps'), + + # RemoteApp System users + path('users//remote-apps//system-users/', api.UserGrantedRemoteAppSystemUsersApi.as_view(), name='user-remote-app-system-users'), + path('users/remote-apps//system-users/', api.UserGrantedRemoteAppSystemUsersApi.as_view(), name='my-remote-app-system-users'), + + # 校验用户对RemoteApp的权限 + path('remote-app-permissions/user/validate/', api.ValidateUserRemoteAppPermissionApi.as_view(), name='validate-user-remote-app-permission'), + + # 用户和RemoteApp变更 + path('remote-app-permissions//users/add/', api.RemoteAppPermissionAddUserApi.as_view(), name='remote-app-permission-add-user'), + path('remote-app-permissions//users/remove/', api.RemoteAppPermissionRemoveUserApi.as_view(), name='remote-app-permission-remove-user'), + path('remote-app-permissions//remote-apps/remove/', api.RemoteAppPermissionRemoveRemoteAppApi.as_view(), name='remote-app-permission-remove-remote-app'), + path('remote-app-permissions//remote-apps/add/', api.RemoteAppPermissionAddRemoteAppApi.as_view(), name='remote-app-permission-add-remote-app'), +] + +remote_app_permission_urlpatterns += router.urls + diff --git a/jumpserver/jumpserver/apps/perms/urls/views_urls.py b/jumpserver/jumpserver/apps/perms/urls/views_urls.py index 964025db30b7cb28aba52f060ac1bb584c392785..f4deba8c8e70968b226e8da2c84497e69794d78a 100644 --- a/jumpserver/jumpserver/apps/perms/urls/views_urls.py +++ b/jumpserver/jumpserver/apps/perms/urls/views_urls.py @@ -23,4 +23,12 @@ urlpatterns = [ path('remote-app-permission//', views.RemoteAppPermissionDetailView.as_view(), name='remote-app-permission-detail'), path('remote-app-permission//user/', views.RemoteAppPermissionUserView.as_view(), name='remote-app-permission-user-list'), path('remote-app-permission//remote-app/', views.RemoteAppPermissionRemoteAppView.as_view(), name='remote-app-permission-remote-app-list'), + + # database-app-permission + path('database-app-permission/', views.DatabaseAppPermissionListView.as_view(), name='database-app-permission-list'), + path('database-app-permission/create/', views.DatabaseAppPermissionCreateView.as_view(), name='database-app-permission-create'), + path('database-app-permission//update/', views.DatabaseAppPermissionUpdateView.as_view(), name='database-app-permission-update'), + path('database-app-permission//', views.DatabaseAppPermissionDetailView.as_view(), name='database-app-permission-detail'), + path('database-app-permission//user/', views.DatabaseAppPermissionUserView.as_view(), name='database-app-permission-user-list'), + path('database-app-permission//database-app/', views.DatabaseAppPermissionDatabaseAppView.as_view(), name='database-app-permission-database-app-list'), ] diff --git a/jumpserver/jumpserver/apps/perms/utils/__init__.py b/jumpserver/jumpserver/apps/perms/utils/__init__.py index 129901afca3bb3cc75dab4584699261e82cf6821..c6581b858d56218da839da0308d95e68f156ef17 100644 --- a/jumpserver/jumpserver/apps/perms/utils/__init__.py +++ b/jumpserver/jumpserver/apps/perms/utils/__init__.py @@ -3,3 +3,4 @@ from .asset_permission import * from .remote_app_permission import * +from .database_app_permission import * diff --git a/jumpserver/jumpserver/apps/perms/utils/asset_permission.py b/jumpserver/jumpserver/apps/perms/utils/asset_permission.py index f71f8366380c4b98fee378226841ac8e52b9cc35..3ba5c68d6e16d28f3828b5cd77ba0003ae84e377 100644 --- a/jumpserver/jumpserver/apps/perms/utils/asset_permission.py +++ b/jumpserver/jumpserver/apps/perms/utils/asset_permission.py @@ -437,7 +437,7 @@ def sort_assets(assets, order_by='hostname', reverse=False): class ParserNode: nodes_only_fields = ("key", "value", "id") - assets_only_fields = ("platform", "hostname", "id", "ip", "protocols") + assets_only_fields = ("hostname", "id", "ip", "protocols", "org_id") system_users_only_fields = ( "id", "name", "username", "protocol", "priority", "login_mode", ) @@ -445,7 +445,6 @@ class ParserNode: @staticmethod def parse_node_to_tree_node(node): name = '{} ({})'.format(node.value, node.assets_amount) - # name = node.value data = { 'id': node.key, 'name': name, @@ -468,7 +467,7 @@ class ParserNode: @staticmethod def parse_asset_to_tree_node(node, asset): icon_skin = 'file' - platform = asset.platform.lower() + platform = asset.platform_base.lower() if platform == 'windows': icon_skin = 'windows' elif platform == 'linux': @@ -482,6 +481,7 @@ class ParserNode: 'isParent': False, 'open': False, 'iconSkin': icon_skin, + 'nocheck': not asset.has_protocol('ssh'), 'meta': { 'type': 'asset', 'asset': { @@ -489,8 +489,8 @@ class ParserNode: 'hostname': asset.hostname, 'ip': asset.ip, 'protocols': asset.protocols_as_list, - 'platform': asset.platform, - "org_name": asset.org_name, + 'platform': asset.platform_base, + 'org_name': asset.org_name, }, } } diff --git a/jumpserver/jumpserver/apps/perms/utils/database_app_permission.py b/jumpserver/jumpserver/apps/perms/utils/database_app_permission.py new file mode 100644 index 0000000000000000000000000000000000000000..9420ab676cc6a6a60aad0ec081443b8902c31a7a --- /dev/null +++ b/jumpserver/jumpserver/apps/perms/utils/database_app_permission.py @@ -0,0 +1,99 @@ +# coding: utf-8 +# + +from django.db.models import Q +from django.utils.translation import ugettext as _ +from orgs.utils import set_to_root_org + +from ..models import DatabaseAppPermission +from common.tree import TreeNode +from applications.models import DatabaseApp +from assets.models import SystemUser + + +__all__ = [ + 'DatabaseAppPermissionUtil', + 'construct_database_apps_tree_root', + 'parse_database_app_to_tree_node' +] + +def get_user_database_app_permissions(user, include_group=True): + if include_group: + groups = user.groups.all() + arg = Q(users=user) | Q(user_groups__in=groups) + else: + arg = Q(users=user) + return DatabaseAppPermission.objects.all().valid().filter(arg) + + +def get_user_group_database_app_permission(user_group): + return DatabaseAppPermission.objects.all().valid().filter( + user_group=user_group + ) + + +class DatabaseAppPermissionUtil: + get_permissions_map = { + 'User': get_user_database_app_permissions, + 'UserGroup': get_user_group_database_app_permission + } + + def __init__(self, obj): + self.object = obj + self.change_org_if_need() + + @staticmethod + def change_org_if_need(): + set_to_root_org() + + @property + def permissions(self): + obj_class = self.object.__class__.__name__ + func = self.get_permissions_map[obj_class] + _permissions = func(self.object) + return _permissions + + def get_database_apps(self): + database_apps = DatabaseApp.objects.filter( + granted_by_permissions__in=self.permissions + ).distinct() + return database_apps + + def get_database_app_system_users(self, database_app): + queryset = self.permissions + kwargs = {'database_apps': database_app} + queryset = queryset.filter(**kwargs) + system_users_ids = queryset.values_list('system_users', flat=True) + system_users_ids = system_users_ids.distinct() + system_users = SystemUser.objects.filter(id__in=system_users_ids) + system_users = system_users.order_by('-priority') + return system_users + + +def construct_database_apps_tree_root(): + tree_root = { + 'id': 'ID_DATABASE_APP_ROOT', + 'name': _('DatabaseApp'), + 'title': 'DatabaseApp', + 'pId': '', + 'open': False, + 'isParent': True, + 'iconSkin': '', + 'meta': {'type': 'database_app'} + } + return TreeNode(**tree_root) + + +def parse_database_app_to_tree_node(parent, database_app): + pid = parent.id if parent else '' + tree_node = { + 'id': database_app.id, + 'name': database_app.name, + 'title': database_app.name, + 'pId': pid, + 'open': False, + 'isParent': False, + 'iconSkin': 'file', + 'meta': {'type': 'database_app'} + } + return TreeNode(**tree_node) diff --git a/jumpserver/jumpserver/apps/perms/utils/remote_app_permission.py b/jumpserver/jumpserver/apps/perms/utils/remote_app_permission.py index bb86f2c807cb2bf4f4927bc0946e9ee6bb8de28b..8f84bf2240cfc1fe4465d152ea87905f378a9b82 100644 --- a/jumpserver/jumpserver/apps/perms/utils/remote_app_permission.py +++ b/jumpserver/jumpserver/apps/perms/utils/remote_app_permission.py @@ -2,6 +2,7 @@ # from django.db.models import Q +from django.utils.translation import ugettext as _ from common.tree import TreeNode from orgs.utils import set_to_root_org @@ -9,7 +10,6 @@ from orgs.utils import set_to_root_org from ..models import RemoteAppPermission from ..hands import RemoteApp, SystemUser - __all__ = [ 'RemoteAppPermissionUtil', 'construct_remote_apps_tree_root', @@ -56,7 +56,7 @@ class RemoteAppPermissionUtil: def get_remote_apps(self): remote_apps = RemoteApp.objects.filter( granted_by_permissions__in=self.permissions - ) + ).distinct() return remote_apps def get_remote_app_system_users(self, remote_app): @@ -73,7 +73,7 @@ class RemoteAppPermissionUtil: def construct_remote_apps_tree_root(): tree_root = { 'id': 'ID_REMOTE_APP_ROOT', - 'name': 'RemoteApp', + 'name': _('RemoteApp'), 'title': 'RemoteApp', 'pId': '', 'open': False, diff --git a/jumpserver/jumpserver/apps/perms/views/__init__.py b/jumpserver/jumpserver/apps/perms/views/__init__.py index 129901afca3bb3cc75dab4584699261e82cf6821..c6581b858d56218da839da0308d95e68f156ef17 100644 --- a/jumpserver/jumpserver/apps/perms/views/__init__.py +++ b/jumpserver/jumpserver/apps/perms/views/__init__.py @@ -3,3 +3,4 @@ from .asset_permission import * from .remote_app_permission import * +from .database_app_permission import * diff --git a/jumpserver/jumpserver/apps/perms/views/asset_permission.py b/jumpserver/jumpserver/apps/perms/views/asset_permission.py index 6130ad2e3c75c75b7e645a87a3221a26e1c72c86..71b4d56042d59e46d7e26107db369e1a6103bc61 100644 --- a/jumpserver/jumpserver/apps/perms/views/asset_permission.py +++ b/jumpserver/jumpserver/apps/perms/views/asset_permission.py @@ -98,9 +98,7 @@ class AssetPermissionDetailView(PermissionsMixin, DetailView): context = { 'app': _('Perms'), 'action': _('Asset permission detail'), - 'system_users_remain': SystemUser.objects.exclude( - granted_by_permissions=self.object - ), + } kwargs.update(context) return super().get_context_data(**kwargs) @@ -131,14 +129,13 @@ class AssetPermissionUserView(PermissionsMixin, return queryset def get_context_data(self, **kwargs): - user_remain = current_org.get_org_members(exclude=('Auditor',)).exclude( - assetpermission=self.object) + users = [str(i) for i in self.object.users.all().values_list('id', flat=True)] user_groups_remain = UserGroup.objects.exclude( assetpermission=self.object) context = { 'app': _('Perms'), 'action': _('Asset permission user list'), - 'users_remain': user_remain, + 'users': users, 'user_groups_remain': user_groups_remain, } kwargs.update(context) @@ -163,13 +160,16 @@ class AssetPermissionAssetView(PermissionsMixin, return queryset def get_context_data(self, **kwargs): - nodes_remain = Node.objects.exclude( - id__in=self.object.nodes.all().values_list('id', flat=True) - ).only('key') + assets = self.object.assets.all().values_list('id', flat=True) + assets = [str(i) for i in assets] + system_users_remain = SystemUser.objects\ + .exclude(granted_by_permissions=self.object)\ + .exclude(protocol=SystemUser.PROTOCOL_MYSQL) context = { 'app': _('Perms'), + 'assets': assets, 'action': _('Asset permission asset list'), - 'nodes_remain': nodes_remain, + 'system_users_remain': system_users_remain, } kwargs.update(context) - return super().get_context_data(**kwargs) \ No newline at end of file + return super().get_context_data(**kwargs) diff --git a/jumpserver/jumpserver/apps/perms/views/database_app_permission.py b/jumpserver/jumpserver/apps/perms/views/database_app_permission.py new file mode 100644 index 0000000000000000000000000000000000000000..50627defea5c327496a36d54af8adc8ef9b52a75 --- /dev/null +++ b/jumpserver/jumpserver/apps/perms/views/database_app_permission.py @@ -0,0 +1,152 @@ +# coding: utf-8 +# + +from django.utils.translation import ugettext as _ + +from django.views.generic import ( + TemplateView, CreateView, UpdateView, DetailView, ListView +) +from django.views.generic.edit import SingleObjectMixin +from django.conf import settings + +from common.permissions import PermissionsMixin, IsOrgAdmin +from users.models import UserGroup +from applications.models import DatabaseApp +from assets.models import SystemUser + +from .. import models, forms + + +__all__ = [ + 'DatabaseAppPermissionListView', 'DatabaseAppPermissionCreateView', + 'DatabaseAppPermissionUpdateView', 'DatabaseAppPermissionDetailView', + 'DatabaseAppPermissionUserView', 'DatabaseAppPermissionDatabaseAppView', +] + + +class DatabaseAppPermissionListView(PermissionsMixin, TemplateView): + template_name = 'perms/database_app_permission_list.html' + permission_classes = [IsOrgAdmin] + + def get_context_data(self, **kwargs): + context = { + 'app': _('Perms'), + 'action': _('DatabaseApp permission list') + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class DatabaseAppPermissionCreateView(PermissionsMixin, CreateView): + template_name = 'perms/database_app_permission_create_update.html' + model = models.DatabaseAppPermission + form_class = forms.DatabaseAppPermissionCreateUpdateForm + permission_classes = [IsOrgAdmin] + + def get_context_data(self, **kwargs): + context = { + 'app': _('Perms'), + 'action': _('Create DatabaseApp permission'), + 'api_action': 'create', + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class DatabaseAppPermissionUpdateView(PermissionsMixin, UpdateView): + template_name = 'perms/database_app_permission_create_update.html' + model = models.DatabaseAppPermission + form_class = forms.DatabaseAppPermissionCreateUpdateForm + permission_classes = [IsOrgAdmin] + + def get_context_data(self, **kwargs): + context = { + 'app': _('Perms'), + 'action': _('Update DatabaseApp permission'), + 'api_action': 'update' + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class DatabaseAppPermissionDetailView(PermissionsMixin, DetailView): + template_name = 'perms/database_app_permission_detail.html' + model = models.DatabaseAppPermission + permission_classes = [IsOrgAdmin] + + def get_context_data(self, **kwargs): + context = { + 'app': _('Perms'), + 'action': _('DatabaseApp permission detail') + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class DatabaseAppPermissionUserView(PermissionsMixin, + SingleObjectMixin, + ListView): + template_name = 'perms/database_app_permission_user.html' + context_object_name = 'database_app_permission' + paginate_by = settings.DISPLAY_PER_PAGE + object = None + permission_classes = [IsOrgAdmin] + + def get(self, request, *args, **kwargs): + self.object = self.get_object(queryset=models.DatabaseAppPermission.objects.all()) + return super().get(request, *args, **kwargs) + + def get_queryset(self): + queryset = list(self.object.get_all_users()) + return queryset + + def get_context_data(self, **kwargs): + users = [str(i) for i in self.object.users.all().values_list('id', flat=True)] + user_groups_remain = UserGroup.objects.exclude( + databaseapppermission=self.object) + context = { + 'app': _('Perms'), + 'action': _('DatabaseApp permission user list'), + 'users': users, + 'user_groups_remain': user_groups_remain, + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class DatabaseAppPermissionDatabaseAppView(PermissionsMixin, + SingleObjectMixin, + ListView): + template_name = 'perms/database_app_permission_database_app.html' + context_object_name = 'database_app_permission' + paginate_by = settings.DISPLAY_PER_PAGE + object = None + permission_classes = [IsOrgAdmin] + + def get(self, request, *args, **kwargs): + self.object = self.get_object( + queryset=models.DatabaseAppPermission.objects.all() + ) + return super().get(request, *args, **kwargs) + + def get_queryset(self): + queryset = list(self.object.get_all_database_apps()) + return queryset + + def get_context_data(self, **kwargs): + database_apps = self.object.get_all_database_apps().values_list('id', flat=True) + database_apps = [str(i) for i in database_apps] + system_users_remain = SystemUser.objects\ + .exclude(granted_by_database_app_permissions=self.object)\ + .filter(protocol=SystemUser.PROTOCOL_MYSQL) + context = { + 'app': _('Perms'), + 'database_apps': database_apps, + 'database_apps_remain': DatabaseApp.objects.exclude( + granted_by_permissions=self.object + ), + 'system_users_remain': system_users_remain, + 'action': _('DatabaseApp permission DatabaseApp list'), + } + kwargs.update(context) + return super().get_context_data(**kwargs) diff --git a/jumpserver/jumpserver/apps/perms/views/remote_app_permission.py b/jumpserver/jumpserver/apps/perms/views/remote_app_permission.py index 742a5fd850223ed0fd994de31c0dfcd822258115..875600c6801301a0198b6e9c70c09c9b5bb4f9fa 100644 --- a/jumpserver/jumpserver/apps/perms/views/remote_app_permission.py +++ b/jumpserver/jumpserver/apps/perms/views/remote_app_permission.py @@ -48,7 +48,7 @@ class RemoteAppPermissionCreateView(PermissionsMixin, CreateView): context = { 'app': _('Perms'), 'action': _('Create RemoteApp permission'), - 'type': 'create' + 'api_action': 'create' } kwargs.update(context) return super().get_context_data(**kwargs) @@ -65,7 +65,7 @@ class RemoteAppPermissionUpdateView(PermissionsMixin, UpdateView): context = { 'app': _('Perms'), 'action': _('Update RemoteApp permission'), - 'type': 'update' + 'api_action': 'update' } kwargs.update(context) return super().get_context_data(**kwargs) @@ -77,12 +77,13 @@ class RemoteAppPermissionDetailView(PermissionsMixin, DetailView): permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): + system_users_remain = SystemUser.objects\ + .exclude(granted_by_remote_app_permissions=self.object)\ + .filter(protocol=SystemUser.PROTOCOL_RDP) context = { 'app': _('Perms'), 'action': _('RemoteApp permission detail'), - 'system_users_remain': SystemUser.objects.exclude( - granted_by_remote_app_permissions=self.object - ), + 'system_users_remain': system_users_remain, } kwargs.update(context) return super().get_context_data(**kwargs) @@ -107,10 +108,10 @@ class RemoteAppPermissionUserView(PermissionsMixin, return queryset def get_context_data(self, **kwargs): - user_remain = current_org.get_org_members(exclude=('Auditor',)).exclude( - remoteapppermission=self.object) - user_groups_remain = UserGroup.objects.exclude( - remoteapppermission=self.object) + user_remain = current_org.get_org_members(exclude=('Auditor',))\ + .exclude(remoteapppermission=self.object) + user_groups_remain = UserGroup.objects\ + .exclude(remoteapppermission=self.object) context = { 'app': _('Perms'), 'action': _('RemoteApp permission user list'), diff --git a/jumpserver/jumpserver/apps/settings/api.py b/jumpserver/jumpserver/apps/settings/api.py index 1fce9a44d3b1f9fe522e8a6603c5b5512ffd5282..354cbdd1dfce3d1756ba923e244bd0de996fdb1b 100644 --- a/jumpserver/jumpserver/apps/settings/api.py +++ b/jumpserver/jumpserver/apps/settings/api.py @@ -245,87 +245,6 @@ class LDAPCacheRefreshAPI(generics.RetrieveAPIView): return Response(data={'msg': 'success'}) -class ReplayStorageCreateAPI(APIView): - permission_classes = (IsSuperUser,) - - def post(self, request): - storage_data = request.data - - if storage_data.get('TYPE') == 'ceph': - port = storage_data.get('PORT') - if port.isdigit(): - storage_data['PORT'] = int(storage_data.get('PORT')) - - storage_name = storage_data.pop('NAME') - data = {storage_name: storage_data} - - if not self.is_valid(storage_data): - return Response({ - "error": _("Error: Account invalid (Please make sure the " - "information such as Access key or Secret key is correct)")}, - status=401 - ) - - Setting.save_storage('TERMINAL_REPLAY_STORAGE', data) - return Response({"msg": _('Create succeed')}, status=200) - - @staticmethod - def is_valid(storage_data): - if storage_data.get('TYPE') == 'server': - return True - storage = jms_storage.get_object_storage(storage_data) - target = 'tests.py' - src = os.path.join(settings.BASE_DIR, 'common', target) - return storage.is_valid(src, target) - - -class ReplayStorageDeleteAPI(APIView): - permission_classes = (IsSuperUser,) - - def post(self, request): - storage_name = str(request.data.get('name')) - Setting.delete_storage('TERMINAL_REPLAY_STORAGE', storage_name) - return Response({"msg": _('Delete succeed')}, status=200) - - -class CommandStorageCreateAPI(APIView): - permission_classes = (IsSuperUser,) - - def post(self, request): - storage_data = request.data - storage_name = storage_data.pop('NAME') - data = {storage_name: storage_data} - if not self.is_valid(storage_data): - return Response( - {"error": _("Error: Account invalid (Please make sure the " - "information such as Access key or Secret key is correct)")}, - status=401 - ) - - Setting.save_storage('TERMINAL_COMMAND_STORAGE', data) - return Response({"msg": _('Create succeed')}, status=200) - - @staticmethod - def is_valid(storage_data): - if storage_data.get('TYPE') == 'server': - return True - try: - storage = jms_storage.get_log_storage(storage_data) - except Exception: - return False - - return storage.ping() - - -class CommandStorageDeleteAPI(APIView): - permission_classes = (IsSuperUser,) - - def post(self, request): - storage_name = str(request.data.get('name')) - Setting.delete_storage('TERMINAL_COMMAND_STORAGE', storage_name) - return Response({"msg": _('Delete succeed')}, status=200) - - class PublicSettingApi(generics.RetrieveAPIView): permission_classes = () serializer_class = PublicSettingSerializer @@ -334,7 +253,8 @@ class PublicSettingApi(generics.RetrieveAPIView): c = settings.CONFIG instance = { "data": { - "WINDOWS_SKIP_ALL_MANUAL_PASSWORD": c.WINDOWS_SKIP_ALL_MANUAL_PASSWORD + "WINDOWS_SKIP_ALL_MANUAL_PASSWORD": c.WINDOWS_SKIP_ALL_MANUAL_PASSWORD, + "SECURITY_MAX_IDLE_TIME": c.SECURITY_MAX_IDLE_TIME, } } return instance diff --git a/jumpserver/jumpserver/apps/settings/forms.py b/jumpserver/jumpserver/apps/settings/forms.py deleted file mode 100644 index 4a115bcacefe541979681780946596404d6fb8fc..0000000000000000000000000000000000000000 --- a/jumpserver/jumpserver/apps/settings/forms.py +++ /dev/null @@ -1,289 +0,0 @@ -# -*- coding: utf-8 -*- -# -import json -from django import forms -from django.utils.translation import ugettext_lazy as _ -from django.db import transaction - -from .models import Setting, settings -from common.fields import ( - FormDictField, FormEncryptCharField, FormEncryptMixin -) - - -class BaseForm(forms.Form): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - for name, field in self.fields.items(): - value = getattr(settings, name, None) - if value is None: # and django_value is None: - continue - - if value is not None: - if isinstance(value, dict): - value = json.dumps(value) - initial_value = value - else: - initial_value = '' - field.initial = initial_value - - def save(self, category="default"): - if not self.is_bound: - raise ValueError("Form is not bound") - - # db_settings = Setting.objects.all() - if not self.is_valid(): - raise ValueError(self.errors) - - with transaction.atomic(): - for name, value in self.cleaned_data.items(): - field = self.fields[name] - if isinstance(field.widget, forms.PasswordInput) and not value: - continue - # if value == getattr(settings, name): - # continue - - encrypted = True if isinstance(field, FormEncryptMixin) else False - try: - setting = Setting.objects.get(name=name) - except Setting.DoesNotExist: - setting = Setting() - setting.name = name - setting.category = category - setting.encrypted = encrypted - setting.cleaned_value = value - setting.save() - - -class BasicSettingForm(BaseForm): - SITE_URL = forms.URLField( - label=_("Current SITE URL"), - help_text="eg: http://jumpserver.abc.com:8080" - ) - USER_GUIDE_URL = forms.URLField( - label=_("User Guide URL"), required=False, - help_text=_("User first login update profile done redirect to it") - ) - EMAIL_SUBJECT_PREFIX = forms.CharField( - max_length=1024, label=_("Email Subject Prefix"), - help_text=_("Tips: Some word will be intercept by mail provider") - ) - - -class EmailSettingForm(BaseForm): - EMAIL_HOST = forms.CharField( - max_length=1024, label=_("SMTP host"), initial='smtp.jumpserver.org' - ) - EMAIL_PORT = forms.CharField(max_length=5, label=_("SMTP port"), initial=25) - EMAIL_HOST_USER = forms.CharField( - max_length=128, label=_("SMTP user"), initial='noreply@jumpserver.org' - ) - EMAIL_HOST_PASSWORD = FormEncryptCharField( - max_length=1024, label=_("SMTP password"), widget=forms.PasswordInput, - required=False, - help_text=_("Tips: Some provider use token except password") - ) - EMAIL_FROM = forms.CharField( - max_length=128, label=_("Send user"), initial='', required=False, - help_text=_( - "Tips: Send mail account, default SMTP account as the send account" - ) - ) - EMAIL_RECIPIENT = forms.CharField( - max_length=128, label=_("Test recipient"), initial='', required=False, - help_text=_("Tips: Used only as a test mail recipient") - ) - EMAIL_USE_SSL = forms.BooleanField( - label=_("Use SSL"), initial=False, required=False, - help_text=_("If SMTP port is 465, may be select") - ) - EMAIL_USE_TLS = forms.BooleanField( - label=_("Use TLS"), initial=False, required=False, - help_text=_("If SMTP port is 587, may be select") - ) - - -class LDAPSettingForm(BaseForm): - AUTH_LDAP_SERVER_URI = forms.CharField( - label=_("LDAP server"), - ) - AUTH_LDAP_BIND_DN = forms.CharField( - required=False, label=_("Bind DN"), - ) - AUTH_LDAP_BIND_PASSWORD = FormEncryptCharField( - label=_("Password"), - widget=forms.PasswordInput, required=False - ) - AUTH_LDAP_SEARCH_OU = forms.CharField( - label=_("User OU"), - help_text=_("Use | split User OUs"), - required=False, - ) - AUTH_LDAP_SEARCH_FILTER = forms.CharField( - label=_("User search filter"), - help_text=_("Choice may be (cn|uid|sAMAccountName)=%(user)s)") - ) - AUTH_LDAP_USER_ATTR_MAP = FormDictField( - label=_("User attr map"), - help_text=_( - "User attr map present how to map LDAP user attr to jumpserver, " - "username,name,email is jumpserver attr" - ), - ) - # AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU - # AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER - # AUTH_LDAP_START_TLS = forms.BooleanField( - # label=_("Use SSL"), required=False - # ) - AUTH_LDAP = forms.BooleanField(label=_("Enable LDAP auth"), required=False) - - -class TerminalSettingForm(BaseForm): - SORT_BY_CHOICES = ( - ('hostname', _('Hostname')), - ('ip', _('IP')), - ) - PAGE_SIZE_CHOICES = ( - ('all', _('All')), - ('auto', _('Auto')), - (10, 10), - (15, 15), - (25, 25), - (50, 50), - ) - TERMINAL_PASSWORD_AUTH = forms.BooleanField( - required=False, label=_("Password auth") - ) - TERMINAL_PUBLIC_KEY_AUTH = forms.BooleanField( - required=False, label=_("Public key auth") - ) - TERMINAL_HEARTBEAT_INTERVAL = forms.IntegerField( - min_value=5, max_value=99999, label=_("Heartbeat interval"), - help_text=_("Units: seconds") - ) - TERMINAL_ASSET_LIST_SORT_BY = forms.ChoiceField( - choices=SORT_BY_CHOICES, label=_("List sort by") - ) - TERMINAL_ASSET_LIST_PAGE_SIZE = forms.ChoiceField( - choices=PAGE_SIZE_CHOICES, label=_("List page size"), - ) - TERMINAL_SESSION_KEEP_DURATION = forms.IntegerField( - min_value=1, max_value=99999, label=_("Session keep duration"), - help_text=_("Units: days, Session, record, command will be delete " - "if more than duration, only in database") - ) - TERMINAL_TELNET_REGEX = forms.CharField( - required=False, label=_("Telnet login regex"), - help_text=_("ex: Last\s*login|success|成功") - ) - - -class TerminalCommandStorage(BaseForm): - pass - - -class SecuritySettingForm(BaseForm): - # MFA global setting - SECURITY_MFA_AUTH = forms.BooleanField( - required=False, label=_("MFA Secondary certification"), - help_text=_( - 'After opening, the user login must use MFA secondary ' - 'authentication (valid for all users, including administrators)' - ) - ) - # Execute commands for user - SECURITY_COMMAND_EXECUTION = forms.BooleanField( - required=False, label=_("Batch execute commands"), - help_text=_("Allow user batch execute commands") - ) - SECURITY_SERVICE_ACCOUNT_REGISTRATION = forms.BooleanField( - required=False, label=_("Service account registration"), - help_text=_("Allow using bootstrap token register service account, " - "when terminal setup, can disable it") - ) - # limit login count - SECURITY_LOGIN_LIMIT_COUNT = forms.IntegerField( - min_value=3, max_value=99999, - label=_("Limit the number of login failures") - ) - # limit login time - SECURITY_LOGIN_LIMIT_TIME = forms.IntegerField( - min_value=5, max_value=99999, label=_("No logon interval"), - help_text=_( - "Tip: (unit/minute) if the user has failed to log in for a limited " - "number of times, no login is allowed during this time interval." - ) - ) - # ssh max idle time - SECURITY_MAX_IDLE_TIME = forms.IntegerField( - min_value=1, max_value=99999, required=False, - label=_("Connection max idle time"), - help_text=_( - 'If idle time more than it, disconnect connection(only ssh now) ' - 'Unit: minute' - ), - ) - # password expiration time - SECURITY_PASSWORD_EXPIRATION_TIME = forms.IntegerField( - min_value=1, max_value=99999, label=_("Password expiration time"), - help_text=_( - "Tip: (unit: day) " - "If the user does not update the password during the time, " - "the user password will expire failure;" - "The password expiration reminder mail will be automatic sent to the user " - "by system within 5 days (daily) before the password expires" - ) - ) - # min length - SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField( - min_value=6, max_value=30, label=_("Password minimum length"), - ) - # upper case - SECURITY_PASSWORD_UPPER_CASE = forms.BooleanField( - required=False, label=_("Must contain capital letters"), - help_text=_( - 'After opening, the user password changes ' - 'and resets must contain uppercase letters') - ) - # lower case - SECURITY_PASSWORD_LOWER_CASE = forms.BooleanField( - required=False, label=_("Must contain lowercase letters"), - help_text=_('After opening, the user password changes ' - 'and resets must contain lowercase letters') - ) - # number - SECURITY_PASSWORD_NUMBER = forms.BooleanField( - required=False, label=_("Must contain numeric characters"), - help_text=_('After opening, the user password changes ' - 'and resets must contain numeric characters') - ) - # special char - SECURITY_PASSWORD_SPECIAL_CHAR = forms.BooleanField( - required=False, label=_("Must contain special characters"), - help_text=_('After opening, the user password changes ' - 'and resets must contain special characters') - ) - - -class EmailContentSettingForm(BaseForm): - EMAIL_CUSTOM_USER_CREATED_SUBJECT = forms.CharField( - max_length=1024, required=False, label=_("Create user email subject"), - help_text=_("Tips: When creating a user, send the subject of the email" - " (eg:Create account successfully)") - ) - EMAIL_CUSTOM_USER_CREATED_HONORIFIC = forms.CharField( - max_length=1024, required=False, label=_("Create user honorific"), - help_text=_("Tips: When creating a user, send the honorific of the " - "email (eg:Hello)") - ) - EMAIL_CUSTOM_USER_CREATED_BODY = forms.CharField( - max_length=4096, required=False, widget=forms.Textarea(), - label=_('Create user email content'), - help_text=_('Tips:When creating a user, send the content of the email') - ) - EMAIL_CUSTOM_USER_CREATED_SIGNATURE = forms.CharField( - max_length=512, required=False, label=_("Signature"), - help_text=_("Tips: Email signature (eg:jumpserver)") - ) - - diff --git a/jumpserver/jumpserver/apps/settings/forms/__init__.py b/jumpserver/jumpserver/apps/settings/forms/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4c5a69e8c0971db32695ef0fed14d16c3772790c --- /dev/null +++ b/jumpserver/jumpserver/apps/settings/forms/__init__.py @@ -0,0 +1,9 @@ +# coding: utf-8 +# + +from .base import * +from .basic import * +from .email import * +from .ldap import * +from .security import * +from .terminal import * diff --git a/jumpserver/jumpserver/apps/settings/forms/base.py b/jumpserver/jumpserver/apps/settings/forms/base.py new file mode 100644 index 0000000000000000000000000000000000000000..b7b32dea6b1041bcc8fa8601129af7ac7838b6b3 --- /dev/null +++ b/jumpserver/jumpserver/apps/settings/forms/base.py @@ -0,0 +1,57 @@ +# coding: utf-8 +# + +import json +from django import forms +from django.db import transaction +from django.conf import settings + +from ..models import Setting +from common.fields import FormEncryptMixin + +__all__ = ['BaseForm'] + + +class BaseForm(forms.Form): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for name, field in self.fields.items(): + value = getattr(settings, name, None) + if value is None: # and django_value is None: + continue + + if value is not None: + if isinstance(value, dict): + value = json.dumps(value) + initial_value = value + else: + initial_value = '' + field.initial = initial_value + + def save(self, category="default"): + if not self.is_bound: + raise ValueError("Form is not bound") + + # db_settings = Setting.objects.all() + if not self.is_valid(): + raise ValueError(self.errors) + + with transaction.atomic(): + for name, value in self.cleaned_data.items(): + field = self.fields[name] + if isinstance(field.widget, forms.PasswordInput) and not value: + continue + # if value == getattr(settings, name): + # continue + + encrypted = True if isinstance(field, FormEncryptMixin) else False + try: + setting = Setting.objects.get(name=name) + except Setting.DoesNotExist: + setting = Setting() + setting.name = name + setting.category = category + setting.encrypted = encrypted + setting.cleaned_value = value + setting.save() + diff --git a/jumpserver/jumpserver/apps/settings/forms/basic.py b/jumpserver/jumpserver/apps/settings/forms/basic.py new file mode 100644 index 0000000000000000000000000000000000000000..dd93c9537e4ab360aa2c85bbb49086d1fef81c98 --- /dev/null +++ b/jumpserver/jumpserver/apps/settings/forms/basic.py @@ -0,0 +1,24 @@ +# coding: utf-8 +# + +from django import forms +from django.utils.translation import ugettext_lazy as _ +from .base import BaseForm + +__all__ = ['BasicSettingForm'] + + +class BasicSettingForm(BaseForm): + SITE_URL = forms.URLField( + label=_("Current SITE URL"), + help_text="eg: http://jumpserver.abc.com:8080" + ) + USER_GUIDE_URL = forms.URLField( + label=_("User Guide URL"), required=False, + help_text=_("User first login update profile done redirect to it") + ) + EMAIL_SUBJECT_PREFIX = forms.CharField( + max_length=1024, label=_("Email Subject Prefix"), + help_text=_("Tips: Some word will be intercept by mail provider") + ) + diff --git a/jumpserver/jumpserver/apps/settings/forms/email.py b/jumpserver/jumpserver/apps/settings/forms/email.py new file mode 100644 index 0000000000000000000000000000000000000000..6fa61148a17cbe603608c372fd643a3e5e85fdfe --- /dev/null +++ b/jumpserver/jumpserver/apps/settings/forms/email.py @@ -0,0 +1,65 @@ +# coding: utf-8 +# + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from common.fields import FormEncryptCharField +from .base import BaseForm + +__all__ = ['EmailSettingForm', 'EmailContentSettingForm'] + + +class EmailSettingForm(BaseForm): + EMAIL_HOST = forms.CharField( + max_length=1024, label=_("SMTP host"), initial='smtp.jumpserver.org' + ) + EMAIL_PORT = forms.CharField(max_length=5, label=_("SMTP port"), initial=25) + EMAIL_HOST_USER = forms.CharField( + max_length=128, label=_("SMTP user"), initial='noreply@jumpserver.org' + ) + EMAIL_HOST_PASSWORD = FormEncryptCharField( + max_length=1024, label=_("SMTP password"), widget=forms.PasswordInput, + required=False, + help_text=_("Tips: Some provider use token except password") + ) + EMAIL_FROM = forms.CharField( + max_length=128, label=_("Send user"), initial='', required=False, + help_text=_( + "Tips: Send mail account, default SMTP account as the send account" + ) + ) + EMAIL_RECIPIENT = forms.CharField( + max_length=128, label=_("Test recipient"), initial='', required=False, + help_text=_("Tips: Used only as a test mail recipient") + ) + EMAIL_USE_SSL = forms.BooleanField( + label=_("Use SSL"), initial=False, required=False, + help_text=_("If SMTP port is 465, may be select") + ) + EMAIL_USE_TLS = forms.BooleanField( + label=_("Use TLS"), initial=False, required=False, + help_text=_("If SMTP port is 587, may be select") + ) + + +class EmailContentSettingForm(BaseForm): + EMAIL_CUSTOM_USER_CREATED_SUBJECT = forms.CharField( + max_length=1024, required=False, label=_("Create user email subject"), + help_text=_("Tips: When creating a user, send the subject of the email" + " (eg:Create account successfully)") + ) + EMAIL_CUSTOM_USER_CREATED_HONORIFIC = forms.CharField( + max_length=1024, required=False, label=_("Create user honorific"), + help_text=_("Tips: When creating a user, send the honorific of the " + "email (eg:Hello)") + ) + EMAIL_CUSTOM_USER_CREATED_BODY = forms.CharField( + max_length=4096, required=False, widget=forms.Textarea(), + label=_('Create user email content'), + help_text=_('Tips:When creating a user, send the content of the email') + ) + EMAIL_CUSTOM_USER_CREATED_SIGNATURE = forms.CharField( + max_length=512, required=False, label=_("Signature"), + help_text=_("Tips: Email signature (eg:jumpserver)") + ) diff --git a/jumpserver/jumpserver/apps/settings/forms/ldap.py b/jumpserver/jumpserver/apps/settings/forms/ldap.py new file mode 100644 index 0000000000000000000000000000000000000000..c44d1c3e4fbcd847b5e3e814fcb41ca3a57766c3 --- /dev/null +++ b/jumpserver/jumpserver/apps/settings/forms/ldap.py @@ -0,0 +1,46 @@ +# coding: utf-8 +# + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from common.fields import FormDictField, FormEncryptCharField +from .base import BaseForm + + +__all__ = ['LDAPSettingForm'] + + +class LDAPSettingForm(BaseForm): + AUTH_LDAP_SERVER_URI = forms.CharField( + label=_("LDAP server"), + ) + AUTH_LDAP_BIND_DN = forms.CharField( + required=False, label=_("Bind DN"), + ) + AUTH_LDAP_BIND_PASSWORD = FormEncryptCharField( + label=_("Password"), + widget=forms.PasswordInput, required=False + ) + AUTH_LDAP_SEARCH_OU = forms.CharField( + label=_("User OU"), + help_text=_("Use | split User OUs"), + required=False, + ) + AUTH_LDAP_SEARCH_FILTER = forms.CharField( + label=_("User search filter"), + help_text=_("Choice may be (cn|uid|sAMAccountName)=%(user)s)") + ) + AUTH_LDAP_USER_ATTR_MAP = FormDictField( + label=_("User attr map"), + help_text=_( + "User attr map present how to map LDAP user attr to jumpserver, " + "username,name,email is jumpserver attr" + ), + ) + # AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU + # AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER + # AUTH_LDAP_START_TLS = forms.BooleanField( + # label=_("Use SSL"), required=False + # ) + AUTH_LDAP = forms.BooleanField(label=_("Enable LDAP auth"), required=False) diff --git a/jumpserver/jumpserver/apps/settings/forms/security.py b/jumpserver/jumpserver/apps/settings/forms/security.py new file mode 100644 index 0000000000000000000000000000000000000000..7b7cc247dcf87f6fcf2e0ee2795b864a8a20faf5 --- /dev/null +++ b/jumpserver/jumpserver/apps/settings/forms/security.py @@ -0,0 +1,93 @@ +# coding: utf-8 +# + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from .base import BaseForm + + +__all__ = ['SecuritySettingForm'] + + +class SecuritySettingForm(BaseForm): + # MFA global setting + SECURITY_MFA_AUTH = forms.BooleanField( + required=False, label=_("MFA Secondary certification"), + help_text=_( + 'After opening, the user login must use MFA secondary ' + 'authentication (valid for all users, including administrators)' + ) + ) + # Execute commands for user + SECURITY_COMMAND_EXECUTION = forms.BooleanField( + required=False, label=_("Batch execute commands"), + help_text=_("Allow user batch execute commands") + ) + SECURITY_SERVICE_ACCOUNT_REGISTRATION = forms.BooleanField( + required=False, label=_("Service account registration"), + help_text=_("Allow using bootstrap token register service account, " + "when terminal setup, can disable it") + ) + # limit login count + SECURITY_LOGIN_LIMIT_COUNT = forms.IntegerField( + min_value=3, max_value=99999, + label=_("Limit the number of login failures") + ) + # limit login time + SECURITY_LOGIN_LIMIT_TIME = forms.IntegerField( + min_value=5, max_value=99999, label=_("No logon interval"), + help_text=_( + "Tip: (unit/minute) if the user has failed to log in for a limited " + "number of times, no login is allowed during this time interval." + ) + ) + # ssh max idle time + SECURITY_MAX_IDLE_TIME = forms.IntegerField( + min_value=1, max_value=99999, required=False, + label=_("Connection max idle time"), + help_text=_( + 'If idle time more than it, disconnect connection ' + 'Unit: minute' + ), + ) + # password expiration time + SECURITY_PASSWORD_EXPIRATION_TIME = forms.IntegerField( + min_value=1, max_value=99999, label=_("Password expiration time"), + help_text=_( + "Tip: (unit: day) " + "If the user does not update the password during the time, " + "the user password will expire failure;" + "The password expiration reminder mail will be automatic sent to the user " + "by system within 5 days (daily) before the password expires" + ) + ) + # min length + SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField( + min_value=6, max_value=30, label=_("Password minimum length"), + ) + # upper case + SECURITY_PASSWORD_UPPER_CASE = forms.BooleanField( + required=False, label=_("Must contain capital letters"), + help_text=_( + 'After opening, the user password changes ' + 'and resets must contain uppercase letters') + ) + # lower case + SECURITY_PASSWORD_LOWER_CASE = forms.BooleanField( + required=False, label=_("Must contain lowercase letters"), + help_text=_('After opening, the user password changes ' + 'and resets must contain lowercase letters') + ) + # number + SECURITY_PASSWORD_NUMBER = forms.BooleanField( + required=False, label=_("Must contain numeric characters"), + help_text=_('After opening, the user password changes ' + 'and resets must contain numeric characters') + ) + # special char + SECURITY_PASSWORD_SPECIAL_CHAR = forms.BooleanField( + required=False, label=_("Must contain special characters"), + help_text=_('After opening, the user password changes ' + 'and resets must contain special characters') + ) diff --git a/jumpserver/jumpserver/apps/settings/forms/terminal.py b/jumpserver/jumpserver/apps/settings/forms/terminal.py new file mode 100644 index 0000000000000000000000000000000000000000..d879e88d2e38880a766dda416dd6a4c8a857477c --- /dev/null +++ b/jumpserver/jumpserver/apps/settings/forms/terminal.py @@ -0,0 +1,50 @@ +# coding: utf-8 +# + + +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from .base import BaseForm + +__all__ = ['TerminalSettingForm'] + + +class TerminalSettingForm(BaseForm): + SORT_BY_CHOICES = ( + ('hostname', _('Hostname')), + ('ip', _('IP')), + ) + PAGE_SIZE_CHOICES = ( + ('all', _('All')), + ('auto', _('Auto')), + (10, 10), + (15, 15), + (25, 25), + (50, 50), + ) + TERMINAL_PASSWORD_AUTH = forms.BooleanField( + required=False, label=_("Password auth") + ) + TERMINAL_PUBLIC_KEY_AUTH = forms.BooleanField( + required=False, label=_("Public key auth") + ) + TERMINAL_HEARTBEAT_INTERVAL = forms.IntegerField( + min_value=5, max_value=99999, label=_("Heartbeat interval"), + help_text=_("Units: seconds") + ) + TERMINAL_ASSET_LIST_SORT_BY = forms.ChoiceField( + choices=SORT_BY_CHOICES, label=_("List sort by") + ) + TERMINAL_ASSET_LIST_PAGE_SIZE = forms.ChoiceField( + choices=PAGE_SIZE_CHOICES, label=_("List page size"), + ) + TERMINAL_SESSION_KEEP_DURATION = forms.IntegerField( + min_value=1, max_value=99999, label=_("Session keep duration"), + help_text=_("Units: days, Session, record, command will be delete " + "if more than duration, only in database") + ) + TERMINAL_TELNET_REGEX = forms.CharField( + required=False, label=_("Telnet login regex"), + help_text=_("ex: Last\s*login|success|成功") + ) diff --git a/jumpserver/jumpserver/apps/settings/models.py b/jumpserver/jumpserver/apps/settings/models.py index 524fa93496f2240f527185bd1a412fb7d740925f..75b56bb54930692707de6833325bca92322b6505 100644 --- a/jumpserver/jumpserver/apps/settings/models.py +++ b/jumpserver/jumpserver/apps/settings/models.py @@ -1,14 +1,11 @@ import json from django.db import models -from django.core.cache import cache from django.db.utils import ProgrammingError, OperationalError from django.utils.translation import ugettext_lazy as _ -from django.conf import settings - -from common.utils import get_signer +from django.core.cache import cache -signer = get_signer() +from common.utils import signer class SettingQuerySet(models.QuerySet): @@ -34,12 +31,28 @@ class Setting(models.Model): comment = models.TextField(verbose_name=_("Comment")) objects = SettingManager() + cache_key_prefix = '_SETTING_' def __str__(self): return self.name - def __getattr__(self, item): - return cache.get(item) + @classmethod + def get(cls, item): + cached = cls.get_from_cache(item) + if cached is not None: + return cached + instances = cls.objects.filter(name=item) + if len(instances) == 1: + s = instances[0] + s.refresh_setting() + return s.cleaned_value + return None + + @classmethod + def get_from_cache(cls, item): + key = cls.cache_key_prefix + item + cached = cache.get(key) + return cached @property def cleaned_value(self): @@ -64,44 +77,6 @@ class Setting(models.Model): except json.JSONDecodeError as e: raise ValueError("Json dump error: {}".format(str(e))) - @classmethod - def save_storage(cls, name, data): - """ - :param name: TERMINAL_REPLAY_STORAGE or TERMINAL_COMMAND_STORAGE - :param data: {} - :return: Setting object - """ - obj = cls.objects.filter(name=name).first() - if not obj: - obj = cls() - obj.name = name - obj.encrypted = True - obj.cleaned_value = data - else: - value = obj.cleaned_value - if value is None: - value = {} - value.update(data) - obj.cleaned_value = value - obj.save() - return obj - - @classmethod - def delete_storage(cls, name, storage_name): - """ - :param name: TERMINAL_REPLAY_STORAGE or TERMINAL_COMMAND_STORAGE - :param storage_name: "" - :return: bool - """ - obj = cls.objects.filter(name=name).first() - if not obj: - return False - value = obj.cleaned_value - value.pop(storage_name, '') - obj.cleaned_value = value - obj.save() - return True - @classmethod def refresh_all_settings(cls): try: @@ -112,16 +87,8 @@ class Setting(models.Model): pass def refresh_setting(self): - setattr(settings, self.name, self.cleaned_value) - if self.name == "AUTH_LDAP": - if self.cleaned_value and settings.AUTH_LDAP_BACKEND not in settings.AUTHENTICATION_BACKENDS: - old_setting = settings.AUTHENTICATION_BACKENDS - old_setting.insert(0, settings.AUTH_LDAP_BACKEND) - settings.AUTHENTICATION_BACKENDS = old_setting - elif not self.cleaned_value and settings.AUTH_LDAP_BACKEND in settings.AUTHENTICATION_BACKENDS: - old_setting = settings.AUTHENTICATION_BACKENDS - old_setting.remove(settings.AUTH_LDAP_BACKEND) - settings.AUTHENTICATION_BACKENDS = old_setting + key = self.cache_key_prefix + self.name + cache.set(key, self.cleaned_value, None) class Meta: db_table = "settings_setting" diff --git a/jumpserver/jumpserver/apps/settings/serializers/__init__.py b/jumpserver/jumpserver/apps/settings/serializers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..04576336426f7b3985e2795dc842150dc80cc1ae --- /dev/null +++ b/jumpserver/jumpserver/apps/settings/serializers/__init__.py @@ -0,0 +1,6 @@ +# coding: utf-8 +# + +from .email import * +from .ldap import * +from .public import * diff --git a/jumpserver/jumpserver/apps/settings/serializers/email.py b/jumpserver/jumpserver/apps/settings/serializers/email.py new file mode 100644 index 0000000000000000000000000000000000000000..6d033f6aaafc47bfa00489c06fb01ed56cae2803 --- /dev/null +++ b/jumpserver/jumpserver/apps/settings/serializers/email.py @@ -0,0 +1,17 @@ +# coding: utf-8 +# + +from rest_framework import serializers + +__all__ = ['MailTestSerializer'] + + +class MailTestSerializer(serializers.Serializer): + EMAIL_HOST = serializers.CharField(max_length=1024, required=True) + EMAIL_PORT = serializers.IntegerField(default=25) + EMAIL_HOST_USER = serializers.CharField(max_length=1024) + EMAIL_HOST_PASSWORD = serializers.CharField(required=False, allow_blank=True) + EMAIL_FROM = serializers.CharField(required=False, allow_blank=True) + EMAIL_RECIPIENT = serializers.CharField(required=False, allow_blank=True) + EMAIL_USE_SSL = serializers.BooleanField(default=False) + EMAIL_USE_TLS = serializers.BooleanField(default=False) diff --git a/jumpserver/jumpserver/apps/settings/serializers.py b/jumpserver/jumpserver/apps/settings/serializers/ldap.py similarity index 54% rename from jumpserver/jumpserver/apps/settings/serializers.py rename to jumpserver/jumpserver/apps/settings/serializers/ldap.py index 1717634f4ec85892a85745c38deb8e4c1bec878c..4009c0705c8db4f409524c5150bc4b7e19f37bf7 100644 --- a/jumpserver/jumpserver/apps/settings/serializers.py +++ b/jumpserver/jumpserver/apps/settings/serializers/ldap.py @@ -1,15 +1,9 @@ -from rest_framework import serializers +# coding: utf-8 +# +from rest_framework import serializers -class MailTestSerializer(serializers.Serializer): - EMAIL_HOST = serializers.CharField(max_length=1024, required=True) - EMAIL_PORT = serializers.IntegerField(default=25) - EMAIL_HOST_USER = serializers.CharField(max_length=1024) - EMAIL_HOST_PASSWORD = serializers.CharField(required=False, allow_blank=True) - EMAIL_FROM = serializers.CharField(required=False, allow_blank=True) - EMAIL_RECIPIENT = serializers.CharField(required=False, allow_blank=True) - EMAIL_USE_SSL = serializers.BooleanField(default=False) - EMAIL_USE_TLS = serializers.BooleanField(default=False) +__all__ = ['LDAPTestSerializer', 'LDAPUserSerializer'] class LDAPTestSerializer(serializers.Serializer): @@ -29,6 +23,3 @@ class LDAPUserSerializer(serializers.Serializer): email = serializers.CharField() existing = serializers.BooleanField(read_only=True) - -class PublicSettingSerializer(serializers.Serializer): - data = serializers.DictField(read_only=True) diff --git a/jumpserver/jumpserver/apps/settings/serializers/public.py b/jumpserver/jumpserver/apps/settings/serializers/public.py new file mode 100644 index 0000000000000000000000000000000000000000..52e39a954f80bdcd05549813aea0a79886aebdd5 --- /dev/null +++ b/jumpserver/jumpserver/apps/settings/serializers/public.py @@ -0,0 +1,10 @@ +# coding: utf-8 +# + +from rest_framework import serializers + +__all__ = ['PublicSettingSerializer'] + + +class PublicSettingSerializer(serializers.Serializer): + data = serializers.DictField(read_only=True) diff --git a/jumpserver/jumpserver/apps/settings/signals_handler.py b/jumpserver/jumpserver/apps/settings/signals_handler.py index c131cc214ac983b8e1945708ddbd7a35429c595b..026d60a78efb0da6e83167e44f017a1ef1f42135 100644 --- a/jumpserver/jumpserver/apps/settings/signals_handler.py +++ b/jumpserver/jumpserver/apps/settings/signals_handler.py @@ -4,9 +4,6 @@ import json from django.dispatch import receiver from django.db.models.signals import post_save, pre_save -from django.conf import LazySettings, empty, global_settings -from django.db.utils import ProgrammingError, OperationalError -from django.core.cache import cache from jumpserver.utils import current_request from common.utils import get_logger, ssh_key_gen @@ -23,56 +20,9 @@ def refresh_settings_on_changed(sender, instance=None, **kwargs): @receiver(django_ready) -def monkey_patch_settings(sender, **kwargs): - logger.debug("Monkey patch settings") - cache_key_prefix = '_SETTING_' - custom_need_cache_settings = [ - 'AUTHENTICATION_BACKENDS', 'TERMINAL_HOST_KEY', - ] - custom_no_cache_settings = [ - 'BASE_DIR', 'VERSION', 'AUTH_OPENID', - ] - django_settings = dir(global_settings) - uncached_settings = [i for i in django_settings if i.isupper()] - uncached_settings = [i for i in uncached_settings if not i.startswith('EMAIL')] - uncached_settings = [i for i in uncached_settings if not i.startswith('SESSION_REDIS')] - uncached_settings = [i for i in uncached_settings if i not in custom_need_cache_settings] - uncached_settings.extend(custom_no_cache_settings) - - def monkey_patch_getattr(self, name): - if name not in uncached_settings: - key = cache_key_prefix + name - cached = cache.get(key) - if cached is not None: - return cached - if self._wrapped is empty: - self._setup(name) - val = getattr(self._wrapped, name) - return val - - def monkey_patch_setattr(self, name, value): - key = cache_key_prefix + name - cache.set(key, value, None) - if name == '_wrapped': - self.__dict__.clear() - else: - self.__dict__.pop(name, None) - super(LazySettings, self).__setattr__(name, value) - - def monkey_patch_delattr(self, name): - super(LazySettings, self).__delattr__(name) - self.__dict__.pop(name, None) - key = cache_key_prefix + name - cache.delete(key) - - try: - cache.delete_pattern(cache_key_prefix+'*') - LazySettings.__getattr__ = monkey_patch_getattr - LazySettings.__setattr__ = monkey_patch_setattr - LazySettings.__delattr__ = monkey_patch_delattr - Setting.refresh_all_settings() - except (ProgrammingError, OperationalError): - pass +def on_django_ready_add_db_config(sender, **kwargs): + from django.conf import settings + settings.DYNAMIC.db_setting = Setting @receiver(django_ready) diff --git a/jumpserver/jumpserver/apps/settings/templates/settings/_setting_tabs.html b/jumpserver/jumpserver/apps/settings/templates/settings/_setting_tabs.html new file mode 100644 index 0000000000000000000000000000000000000000..b012a6669fba175fff6a8fc29a48dc324aa2c06c --- /dev/null +++ b/jumpserver/jumpserver/apps/settings/templates/settings/_setting_tabs.html @@ -0,0 +1,37 @@ +{% load i18n %} + + + diff --git a/jumpserver/jumpserver/apps/settings/templates/settings/basic_setting.html b/jumpserver/jumpserver/apps/settings/templates/settings/basic_setting.html index 4c26e8bb3f322036372442f816369c2f7daae4b9..ac8cabb8b170b8d42048104c256fb25251b1424c 100644 --- a/jumpserver/jumpserver/apps/settings/templates/settings/basic_setting.html +++ b/jumpserver/jumpserver/apps/settings/templates/settings/basic_setting.html @@ -10,26 +10,7 @@
    diff --git a/jumpserver/jumpserver/apps/settings/templates/settings/command_storage_create.html b/jumpserver/jumpserver/apps/settings/templates/settings/command_storage_create.html deleted file mode 100644 index 9c60e2d85a50935ee41cfdc08e790a37821507bb..0000000000000000000000000000000000000000 --- a/jumpserver/jumpserver/apps/settings/templates/settings/command_storage_create.html +++ /dev/null @@ -1,175 +0,0 @@ -{#{% extends 'base.html' %}#} -{% extends '_base_create_update.html' %} -{% load static %} -{% load bootstrap3 %} -{% load i18n %} -{% load common_tags %} - -{% block content %} -
    -
    -
    -
    -
    -
    {{ action }}
    - -
    - -
    -
    -
    - -
    - -
    -
    - -
    - -
    - -
    -
    -
    - - -{# #} - - - - -
    -
    -
    - - {% trans 'Submit' %} -
    -
    -
    -
    -
    -
    -
    -
    -{% endblock %} - -{% block custom_foot_js %} - -{% endblock %} diff --git a/jumpserver/jumpserver/apps/settings/templates/settings/email_content_setting.html b/jumpserver/jumpserver/apps/settings/templates/settings/email_content_setting.html index 16cac426e393c78ac25bfde7123181dc43ce03ee..c2c5b2720a94c352a0ee9115a80320f4246f40dc 100644 --- a/jumpserver/jumpserver/apps/settings/templates/settings/email_content_setting.html +++ b/jumpserver/jumpserver/apps/settings/templates/settings/email_content_setting.html @@ -10,53 +10,33 @@
    -
    -
    - {% if form.non_field_errors %} -
    - {{ form.non_field_errors }} -
    - {% endif %} - {% csrf_token %} +
    + + {% if form.non_field_errors %} +
    + {{ form.non_field_errors }} +
    + {% endif %} + {% csrf_token %} -

    {% trans "Create User setting" %}

    - {% bootstrap_field form.EMAIL_CUSTOM_USER_CREATED_SUBJECT layout="horizontal" %} - {% bootstrap_field form.EMAIL_CUSTOM_USER_CREATED_HONORIFIC layout="horizontal" %} - {% bootstrap_field form.EMAIL_CUSTOM_USER_CREATED_BODY layout="horizontal" %} - {% bootstrap_field form.EMAIL_CUSTOM_USER_CREATED_SIGNATURE layout="horizontal" %} -
    +

    {% trans "Create User setting" %}

    + {% bootstrap_field form.EMAIL_CUSTOM_USER_CREATED_SUBJECT layout="horizontal" %} + {% bootstrap_field form.EMAIL_CUSTOM_USER_CREATED_HONORIFIC layout="horizontal" %} + {% bootstrap_field form.EMAIL_CUSTOM_USER_CREATED_BODY layout="horizontal" %} + {% bootstrap_field form.EMAIL_CUSTOM_USER_CREATED_SIGNATURE layout="horizontal" %} +
    -
    -
    - - -
    +
    +
    + +
    -
    +
    diff --git a/jumpserver/jumpserver/apps/settings/templates/settings/email_setting.html b/jumpserver/jumpserver/apps/settings/templates/settings/email_setting.html index 40ce9f4cc15ad61c8068658cda85cbdf7ec4cfb9..d62a921cd4df64e081fa9bdee2c2d48086e2e9b0 100644 --- a/jumpserver/jumpserver/apps/settings/templates/settings/email_setting.html +++ b/jumpserver/jumpserver/apps/settings/templates/settings/email_setting.html @@ -10,26 +10,7 @@
    diff --git a/jumpserver/jumpserver/apps/settings/templates/settings/ldap_setting.html b/jumpserver/jumpserver/apps/settings/templates/settings/ldap_setting.html index 5da66405db48d542b89ca7c07b5bec08f72e08e7..943a6932212dc4df65a205a31b5d8b892373a906 100644 --- a/jumpserver/jumpserver/apps/settings/templates/settings/ldap_setting.html +++ b/jumpserver/jumpserver/apps/settings/templates/settings/ldap_setting.html @@ -10,26 +10,7 @@
    diff --git a/jumpserver/jumpserver/apps/settings/templates/settings/replay_storage_create.html b/jumpserver/jumpserver/apps/settings/templates/settings/replay_storage_create.html deleted file mode 100644 index 6521a730a5cb3e218f5cfb53f824064e9ae496b4..0000000000000000000000000000000000000000 --- a/jumpserver/jumpserver/apps/settings/templates/settings/replay_storage_create.html +++ /dev/null @@ -1,277 +0,0 @@ -{#{% extends 'base.html' %}#} -{% extends '_base_create_update.html' %} -{% load static %} -{% load bootstrap3 %} -{% load i18n %} -{% load common_tags %} - -{% block content %} -
    -
    -
    -
    -
    -
    {{ action }}
    - -
    - -
    -
    -
    - -
    - -
    -
    - -
    - -
    - -
    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - -
    -
    -
    - - {% trans 'Submit' %} -
    -
    -
    -
    -
    -
    -
    -
    -{% endblock %} - -{% block custom_foot_js %} - -{% endblock %} diff --git a/jumpserver/jumpserver/apps/settings/templates/settings/security_setting.html b/jumpserver/jumpserver/apps/settings/templates/settings/security_setting.html index 48206676d3e747b3ffefdb36ed2e7e5895842a79..663632a726e1b616a4e67e84bd67bd212a55fa0c 100644 --- a/jumpserver/jumpserver/apps/settings/templates/settings/security_setting.html +++ b/jumpserver/jumpserver/apps/settings/templates/settings/security_setting.html @@ -10,26 +10,7 @@
    @@ -44,7 +25,7 @@

    {% trans "Security setting" %}

    {% for field in form %} - {% if forloop.counter == 6 %} + {% if forloop.counter == 8 %}

    {% trans "Password check rule" %}

    {% endif %} diff --git a/jumpserver/jumpserver/apps/settings/templates/settings/terminal_setting.html b/jumpserver/jumpserver/apps/settings/templates/settings/terminal_setting.html index 3a9a4973d84bb22ec075141cd96116957cf6a0ff..a0b35aabb4f22d461b5c2f349add59a94f402d1f 100644 --- a/jumpserver/jumpserver/apps/settings/templates/settings/terminal_setting.html +++ b/jumpserver/jumpserver/apps/settings/templates/settings/terminal_setting.html @@ -3,6 +3,11 @@ {% load bootstrap3 %} {% load i18n %} {% load common_tags %} +{% block help_message %} + {% trans "Command and Replay storage configuration migrated to" %} + {% trans "Sessions -> Terminal -> Storage configuration" %} + {% trans 'Here' %} +{% endblock %} {% block content %}
    @@ -10,30 +15,7 @@
    @@ -65,7 +47,7 @@
    {% endif %} {% endfor %} - +
    @@ -73,53 +55,6 @@ type="submit">{% trans 'Submit' %}
    - -
    - -

    {% trans "Command storage" %}

    - - - - - - - - - - {% for name, setting in command_storage.items %} - - - - - - {% endfor %} - -
    {% trans 'Name' %}{% trans 'Type' %}{% trans 'Action' %}
    {{ name }}{{ setting.TYPE }}{% trans 'Delete' %}
    - {% trans 'Add' %} - -
    -

    {% trans "Replay storage" %}

    - - - - - - - - - - {% for name, setting in replay_storage.items %} - - - - - - {% endfor %} - -
    {% trans 'Name' %}{% trans 'Type' %}{% trans 'Action' %}
    {{ name }}{{ setting.TYPE }}{% trans 'Delete' %}
    - {% trans 'Add' %} - -
    @@ -131,60 +66,7 @@ {% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/jumpserver/jumpserver/apps/settings/urls/api_urls.py b/jumpserver/jumpserver/apps/settings/urls/api_urls.py index 544de39413d76f64e5fbf72409a6b3f0b5f7ba00..abee1c0d08837d557988f55d8978f371c4e201cf 100644 --- a/jumpserver/jumpserver/apps/settings/urls/api_urls.py +++ b/jumpserver/jumpserver/apps/settings/urls/api_urls.py @@ -12,9 +12,6 @@ urlpatterns = [ path('ldap/users/', api.LDAPUserListApi.as_view(), name='ldap-user-list'), path('ldap/users/import/', api.LDAPUserImportAPI.as_view(), name='ldap-user-import'), path('ldap/cache/refresh/', api.LDAPCacheRefreshAPI.as_view(), name='ldap-cache-refresh'), - path('terminal/replay-storage/create/', api.ReplayStorageCreateAPI.as_view(), name='replay-storage-create'), - path('terminal/replay-storage/delete/', api.ReplayStorageDeleteAPI.as_view(), name='replay-storage-delete'), - path('terminal/command-storage/create/', api.CommandStorageCreateAPI.as_view(), name='command-storage-create'), - path('terminal/command-storage/delete/', api.CommandStorageDeleteAPI.as_view(), name='command-storage-delete'), + path('public/', api.PublicSettingApi.as_view(), name='public-setting'), ] diff --git a/jumpserver/jumpserver/apps/settings/urls/view_urls.py b/jumpserver/jumpserver/apps/settings/urls/view_urls.py index eac18a432aa8a652980e50fc35827bf98e4b0ab8..6a1c5baaf0751063549187298d3fe61eb8aaf35a 100644 --- a/jumpserver/jumpserver/apps/settings/urls/view_urls.py +++ b/jumpserver/jumpserver/apps/settings/urls/view_urls.py @@ -12,7 +12,5 @@ urlpatterns = [ url(r'^email-content/$', views.EmailContentSettingView.as_view(), name='email-content-setting'), url(r'^ldap/$', views.LDAPSettingView.as_view(), name='ldap-setting'), url(r'^terminal/$', views.TerminalSettingView.as_view(), name='terminal-setting'), - url(r'^terminal/replay-storage/create$', views.ReplayStorageCreateView.as_view(), name='replay-storage-create'), - url(r'^terminal/command-storage/create$', views.CommandStorageCreateView.as_view(), name='command-storage-create'), url(r'^security/$', views.SecuritySettingView.as_view(), name='security-setting'), ] diff --git a/jumpserver/jumpserver/apps/settings/views.py b/jumpserver/jumpserver/apps/settings/views.py index 2442f074e3cebe7e5f11c50b608d02ad0a74c7e7..6ebbeef9777878d4080d2c39d41951daa2e26dd6 100644 --- a/jumpserver/jumpserver/apps/settings/views.py +++ b/jumpserver/jumpserver/apps/settings/views.py @@ -4,7 +4,6 @@ from django.contrib import messages from django.utils.translation import ugettext as _ from common.permissions import PermissionsMixin, IsSuperUser -from common import utils from .utils import LDAPSyncUtil from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \ TerminalSettingForm, SecuritySettingForm, EmailContentSettingForm @@ -98,8 +97,9 @@ class TerminalSettingView(PermissionsMixin, TemplateView): permission_classes = [IsSuperUser] def get_context_data(self, **kwargs): - command_storage = utils.get_command_storage_setting() - replay_storage = utils.get_replay_storage_setting() + from terminal.models import CommandStorage, ReplayStorage + command_storage = CommandStorage.objects.all() + replay_storage = ReplayStorage.objects.all() context = { 'app': _('Settings'), @@ -124,32 +124,6 @@ class TerminalSettingView(PermissionsMixin, TemplateView): return render(request, self.template_name, context) -class ReplayStorageCreateView(PermissionsMixin, TemplateView): - template_name = 'settings/replay_storage_create.html' - permission_classes = [IsSuperUser] - - def get_context_data(self, **kwargs): - context = { - 'app': _('Settings'), - 'action': _('Create replay storage') - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - -class CommandStorageCreateView(PermissionsMixin, TemplateView): - template_name = 'settings/command_storage_create.html' - permission_classes = [IsSuperUser] - - def get_context_data(self, **kwargs): - context = { - 'app': _('Settings'), - 'action': _('Create command storage') - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - class SecuritySettingView(PermissionsMixin, TemplateView): form_class = SecuritySettingForm template_name = "settings/security_setting.html" diff --git a/jumpserver/jumpserver/apps/static/css/jumpserver.css b/jumpserver/jumpserver/apps/static/css/jumpserver.css index 02e738d247357dd01ee00d02e24814a6828838c7..02b59aae614009e112a32d27989c0d008db7f7a6 100644 --- a/jumpserver/jumpserver/apps/static/css/jumpserver.css +++ b/jumpserver/jumpserver/apps/static/css/jumpserver.css @@ -474,3 +474,87 @@ span.select2-selection__placeholder { .p-r-5 { padding-right: 5px; } + +.dropdown-submenu { + position: relative; +} + +.dropdown-submenu>.dropdown-menu { + top: 0; + left: 100%; + margin-top: -6px; + margin-left: -1px; + -webkit-border-radius: 0 6px 6px 6px; + -moz-border-radius: 0 6px 6px; + border-radius: 0 6px 6px 6px; +} + +.dropdown-submenu:hover>.dropdown-menu { + display: block; +} + +.dropdown-submenu>a:after { + display: block; + content: " "; + float: right; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + border-width: 5px 0 5px 5px; + border-left-color: #ccc; + margin-top: 5px; + margin-right: -10px; +} + +.dropdown-submenu:hover>a:after { + border-left-color: #fff; +} + +.dropdown-submenu.pull-left { + float: none; +} + +.dropdown-submenu.pull-left>.dropdown-menu { + left: -100px; + margin-left: 10px; + -webkit-border-radius: 6px 0 6px 6px; + -moz-border-radius: 6px 0 6px 6px; + border-radius: 6px 0 6px 6px; +} + + +.bootstrap-tagsinput { + border: 1px solid #e5e6e7; + box-shadow: none; + padding: 4px 6px; + cursor: text; +} + +/*.bootstrap-tagsinput {*/ +/* background-color: #fff;*/ +/* border: 1px solid #ccc;*/ +/* box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);*/ +/* display: inline-block;*/ +/* color: #555;*/ +/* vertical-align: middle;*/ +/* border-radius: 4px;*/ +/* max-width: 100%;*/ +/* line-height: 22px;*/ +/*}*/ + +.bootstrap-tagsinput input { + border: none; + box-shadow: none; + outline: none; + background-color: transparent; + padding: 0 6px; + margin: 0; + width: auto; + height: 22px; + max-width: inherit; +} + +table.table-striped.table-bordered { + width: 100% !important; +} diff --git a/jumpserver/jumpserver/apps/static/css/plugins/ladda/ladda-themeless.min.css b/jumpserver/jumpserver/apps/static/css/plugins/ladda/ladda-themeless.min.css new file mode 100755 index 0000000000000000000000000000000000000000..6dee68811154eb786862f8a5d0d23bc7a2e8c167 --- /dev/null +++ b/jumpserver/jumpserver/apps/static/css/plugins/ladda/ladda-themeless.min.css @@ -0,0 +1,7 @@ +/*! + * Ladda + * http://lab.hakim.se/ladda + * MIT licensed + * + * Copyright (C) 2015 Hakim El Hattab, http://hakim.se + */.ladda-button{position:relative}.ladda-button .ladda-spinner{position:absolute;z-index:2;display:inline-block;width:32px;height:32px;top:50%;margin-top:0;opacity:0;pointer-events:none}.ladda-button .ladda-label{position:relative;z-index:3}.ladda-button .ladda-progress{position:absolute;width:0;height:100%;left:0;top:0;background:rgba(0,0,0,0.2);visibility:hidden;opacity:0;-webkit-transition:0.1s linear all !important;-moz-transition:0.1s linear all !important;-ms-transition:0.1s linear all !important;-o-transition:0.1s linear all !important;transition:0.1s linear all !important}.ladda-button[data-loading] .ladda-progress{opacity:1;visibility:visible}.ladda-button,.ladda-button .ladda-spinner,.ladda-button .ladda-label{-webkit-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;-moz-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;-ms-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;-o-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important}.ladda-button[data-style=zoom-in],.ladda-button[data-style=zoom-in] .ladda-spinner,.ladda-button[data-style=zoom-in] .ladda-label,.ladda-button[data-style=zoom-out],.ladda-button[data-style=zoom-out] .ladda-spinner,.ladda-button[data-style=zoom-out] .ladda-label{-webkit-transition:0.3s ease all !important;-moz-transition:0.3s ease all !important;-ms-transition:0.3s ease all !important;-o-transition:0.3s ease all !important;transition:0.3s ease all !important}.ladda-button[data-style=expand-right] .ladda-spinner{right:-6px}.ladda-button[data-style=expand-right][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-right][data-size="xs"] .ladda-spinner{right:-12px}.ladda-button[data-style=expand-right][data-loading]{padding-right:56px}.ladda-button[data-style=expand-right][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=expand-right][data-loading][data-size="s"],.ladda-button[data-style=expand-right][data-loading][data-size="xs"]{padding-right:40px}.ladda-button[data-style=expand-left] .ladda-spinner{left:26px}.ladda-button[data-style=expand-left][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-left][data-size="xs"] .ladda-spinner{left:4px}.ladda-button[data-style=expand-left][data-loading]{padding-left:56px}.ladda-button[data-style=expand-left][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=expand-left][data-loading][data-size="s"],.ladda-button[data-style=expand-left][data-loading][data-size="xs"]{padding-left:40px}.ladda-button[data-style=expand-up]{overflow:hidden}.ladda-button[data-style=expand-up] .ladda-spinner{top:-32px;left:50%;margin-left:0}.ladda-button[data-style=expand-up][data-loading]{padding-top:54px}.ladda-button[data-style=expand-up][data-loading] .ladda-spinner{opacity:1;top:26px;margin-top:0}.ladda-button[data-style=expand-up][data-loading][data-size="s"],.ladda-button[data-style=expand-up][data-loading][data-size="xs"]{padding-top:32px}.ladda-button[data-style=expand-up][data-loading][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-up][data-loading][data-size="xs"] .ladda-spinner{top:4px}.ladda-button[data-style=expand-down]{overflow:hidden}.ladda-button[data-style=expand-down] .ladda-spinner{top:62px;left:50%;margin-left:0}.ladda-button[data-style=expand-down][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-down][data-size="xs"] .ladda-spinner{top:40px}.ladda-button[data-style=expand-down][data-loading]{padding-bottom:54px}.ladda-button[data-style=expand-down][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=expand-down][data-loading][data-size="s"],.ladda-button[data-style=expand-down][data-loading][data-size="xs"]{padding-bottom:32px}.ladda-button[data-style=slide-left]{overflow:hidden}.ladda-button[data-style=slide-left] .ladda-label{position:relative}.ladda-button[data-style=slide-left] .ladda-spinner{left:100%;margin-left:0}.ladda-button[data-style=slide-left][data-loading] .ladda-label{opacity:0;left:-100%}.ladda-button[data-style=slide-left][data-loading] .ladda-spinner{opacity:1;left:50%}.ladda-button[data-style=slide-right]{overflow:hidden}.ladda-button[data-style=slide-right] .ladda-label{position:relative}.ladda-button[data-style=slide-right] .ladda-spinner{right:100%;margin-left:0;left:16px}.ladda-button[data-style=slide-right][data-loading] .ladda-label{opacity:0;left:100%}.ladda-button[data-style=slide-right][data-loading] .ladda-spinner{opacity:1;left:50%}.ladda-button[data-style=slide-up]{overflow:hidden}.ladda-button[data-style=slide-up] .ladda-label{position:relative}.ladda-button[data-style=slide-up] .ladda-spinner{left:50%;margin-left:0;margin-top:1em}.ladda-button[data-style=slide-up][data-loading] .ladda-label{opacity:0;top:-1em}.ladda-button[data-style=slide-up][data-loading] .ladda-spinner{opacity:1;margin-top:0}.ladda-button[data-style=slide-down]{overflow:hidden}.ladda-button[data-style=slide-down] .ladda-label{position:relative}.ladda-button[data-style=slide-down] .ladda-spinner{left:50%;margin-left:0;margin-top:-2em}.ladda-button[data-style=slide-down][data-loading] .ladda-label{opacity:0;top:1em}.ladda-button[data-style=slide-down][data-loading] .ladda-spinner{opacity:1;margin-top:0}.ladda-button[data-style=zoom-out]{overflow:hidden}.ladda-button[data-style=zoom-out] .ladda-spinner{left:50%;margin-left:32px;-webkit-transform:scale(2.5);-moz-transform:scale(2.5);-ms-transform:scale(2.5);-o-transform:scale(2.5);transform:scale(2.5)}.ladda-button[data-style=zoom-out] .ladda-label{position:relative;display:inline-block}.ladda-button[data-style=zoom-out][data-loading] .ladda-label{opacity:0;-webkit-transform:scale(0.5);-moz-transform:scale(0.5);-ms-transform:scale(0.5);-o-transform:scale(0.5);transform:scale(0.5)}.ladda-button[data-style=zoom-out][data-loading] .ladda-spinner{opacity:1;margin-left:0;-webkit-transform:none;-moz-transform:none;-ms-transform:none;-o-transform:none;transform:none}.ladda-button[data-style=zoom-in]{overflow:hidden}.ladda-button[data-style=zoom-in] .ladda-spinner{left:50%;margin-left:-16px;-webkit-transform:scale(0.2);-moz-transform:scale(0.2);-ms-transform:scale(0.2);-o-transform:scale(0.2);transform:scale(0.2)}.ladda-button[data-style=zoom-in] .ladda-label{position:relative;display:inline-block}.ladda-button[data-style=zoom-in][data-loading] .ladda-label{opacity:0;-webkit-transform:scale(2.2);-moz-transform:scale(2.2);-ms-transform:scale(2.2);-o-transform:scale(2.2);transform:scale(2.2)}.ladda-button[data-style=zoom-in][data-loading] .ladda-spinner{opacity:1;margin-left:0;-webkit-transform:none;-moz-transform:none;-ms-transform:none;-o-transform:none;transform:none}.ladda-button[data-style=contract]{overflow:hidden;width:100px}.ladda-button[data-style=contract] .ladda-spinner{left:50%;margin-left:0}.ladda-button[data-style=contract][data-loading]{border-radius:50%;width:52px}.ladda-button[data-style=contract][data-loading] .ladda-label{opacity:0}.ladda-button[data-style=contract][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=contract-overlay]{overflow:hidden;width:100px;box-shadow:0px 0px 0px 2000px transparent}.ladda-button[data-style=contract-overlay] .ladda-spinner{left:50%;margin-left:0}.ladda-button[data-style=contract-overlay][data-loading]{border-radius:50%;width:52px;box-shadow:0px 0px 0px 2000px rgba(0,0,0,0.8)}.ladda-button[data-style=contract-overlay][data-loading] .ladda-label{opacity:0}.ladda-button[data-style=contract-overlay][data-loading] .ladda-spinner{opacity:1} diff --git a/jumpserver/jumpserver/apps/static/css/plugins/ladda/ladda.min.css b/jumpserver/jumpserver/apps/static/css/plugins/ladda/ladda.min.css new file mode 100755 index 0000000000000000000000000000000000000000..7a42a10f06418f71a2ae7dfcca3c55f16acf1fb4 --- /dev/null +++ b/jumpserver/jumpserver/apps/static/css/plugins/ladda/ladda.min.css @@ -0,0 +1,9 @@ +/*! + * Ladda including the default theme. + *//*! + * Ladda + * http://lab.hakim.se/ladda + * MIT licensed + * + * Copyright (C) 2015 Hakim El Hattab, http://hakim.se + */.ladda-button{position:relative}.ladda-button .ladda-spinner{position:absolute;z-index:2;display:inline-block;width:32px;height:32px;top:50%;margin-top:0;opacity:0;pointer-events:none}.ladda-button .ladda-label{position:relative;z-index:3}.ladda-button .ladda-progress{position:absolute;width:0;height:100%;left:0;top:0;background:rgba(0,0,0,0.2);visibility:hidden;opacity:0;-webkit-transition:0.1s linear all !important;-moz-transition:0.1s linear all !important;-ms-transition:0.1s linear all !important;-o-transition:0.1s linear all !important;transition:0.1s linear all !important}.ladda-button[data-loading] .ladda-progress{opacity:1;visibility:visible}.ladda-button,.ladda-button .ladda-spinner,.ladda-button .ladda-label{-webkit-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;-moz-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;-ms-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;-o-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important}.ladda-button[data-style=zoom-in],.ladda-button[data-style=zoom-in] .ladda-spinner,.ladda-button[data-style=zoom-in] .ladda-label,.ladda-button[data-style=zoom-out],.ladda-button[data-style=zoom-out] .ladda-spinner,.ladda-button[data-style=zoom-out] .ladda-label{-webkit-transition:0.3s ease all !important;-moz-transition:0.3s ease all !important;-ms-transition:0.3s ease all !important;-o-transition:0.3s ease all !important;transition:0.3s ease all !important}.ladda-button[data-style=expand-right] .ladda-spinner{right:-6px}.ladda-button[data-style=expand-right][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-right][data-size="xs"] .ladda-spinner{right:-12px}.ladda-button[data-style=expand-right][data-loading]{padding-right:56px}.ladda-button[data-style=expand-right][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=expand-right][data-loading][data-size="s"],.ladda-button[data-style=expand-right][data-loading][data-size="xs"]{padding-right:40px}.ladda-button[data-style=expand-left] .ladda-spinner{left:26px}.ladda-button[data-style=expand-left][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-left][data-size="xs"] .ladda-spinner{left:4px}.ladda-button[data-style=expand-left][data-loading]{padding-left:56px}.ladda-button[data-style=expand-left][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=expand-left][data-loading][data-size="s"],.ladda-button[data-style=expand-left][data-loading][data-size="xs"]{padding-left:40px}.ladda-button[data-style=expand-up]{overflow:hidden}.ladda-button[data-style=expand-up] .ladda-spinner{top:-32px;left:50%;margin-left:0}.ladda-button[data-style=expand-up][data-loading]{padding-top:54px}.ladda-button[data-style=expand-up][data-loading] .ladda-spinner{opacity:1;top:26px;margin-top:0}.ladda-button[data-style=expand-up][data-loading][data-size="s"],.ladda-button[data-style=expand-up][data-loading][data-size="xs"]{padding-top:32px}.ladda-button[data-style=expand-up][data-loading][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-up][data-loading][data-size="xs"] .ladda-spinner{top:4px}.ladda-button[data-style=expand-down]{overflow:hidden}.ladda-button[data-style=expand-down] .ladda-spinner{top:62px;left:50%;margin-left:0}.ladda-button[data-style=expand-down][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-down][data-size="xs"] .ladda-spinner{top:40px}.ladda-button[data-style=expand-down][data-loading]{padding-bottom:54px}.ladda-button[data-style=expand-down][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=expand-down][data-loading][data-size="s"],.ladda-button[data-style=expand-down][data-loading][data-size="xs"]{padding-bottom:32px}.ladda-button[data-style=slide-left]{overflow:hidden}.ladda-button[data-style=slide-left] .ladda-label{position:relative}.ladda-button[data-style=slide-left] .ladda-spinner{left:100%;margin-left:0}.ladda-button[data-style=slide-left][data-loading] .ladda-label{opacity:0;left:-100%}.ladda-button[data-style=slide-left][data-loading] .ladda-spinner{opacity:1;left:50%}.ladda-button[data-style=slide-right]{overflow:hidden}.ladda-button[data-style=slide-right] .ladda-label{position:relative}.ladda-button[data-style=slide-right] .ladda-spinner{right:100%;margin-left:0;left:16px}.ladda-button[data-style=slide-right][data-loading] .ladda-label{opacity:0;left:100%}.ladda-button[data-style=slide-right][data-loading] .ladda-spinner{opacity:1;left:50%}.ladda-button[data-style=slide-up]{overflow:hidden}.ladda-button[data-style=slide-up] .ladda-label{position:relative}.ladda-button[data-style=slide-up] .ladda-spinner{left:50%;margin-left:0;margin-top:1em}.ladda-button[data-style=slide-up][data-loading] .ladda-label{opacity:0;top:-1em}.ladda-button[data-style=slide-up][data-loading] .ladda-spinner{opacity:1;margin-top:0}.ladda-button[data-style=slide-down]{overflow:hidden}.ladda-button[data-style=slide-down] .ladda-label{position:relative}.ladda-button[data-style=slide-down] .ladda-spinner{left:50%;margin-left:0;margin-top:-2em}.ladda-button[data-style=slide-down][data-loading] .ladda-label{opacity:0;top:1em}.ladda-button[data-style=slide-down][data-loading] .ladda-spinner{opacity:1;margin-top:0}.ladda-button[data-style=zoom-out]{overflow:hidden}.ladda-button[data-style=zoom-out] .ladda-spinner{left:50%;margin-left:32px;-webkit-transform:scale(2.5);-moz-transform:scale(2.5);-ms-transform:scale(2.5);-o-transform:scale(2.5);transform:scale(2.5)}.ladda-button[data-style=zoom-out] .ladda-label{position:relative;display:inline-block}.ladda-button[data-style=zoom-out][data-loading] .ladda-label{opacity:0;-webkit-transform:scale(0.5);-moz-transform:scale(0.5);-ms-transform:scale(0.5);-o-transform:scale(0.5);transform:scale(0.5)}.ladda-button[data-style=zoom-out][data-loading] .ladda-spinner{opacity:1;margin-left:0;-webkit-transform:none;-moz-transform:none;-ms-transform:none;-o-transform:none;transform:none}.ladda-button[data-style=zoom-in]{overflow:hidden}.ladda-button[data-style=zoom-in] .ladda-spinner{left:50%;margin-left:-16px;-webkit-transform:scale(0.2);-moz-transform:scale(0.2);-ms-transform:scale(0.2);-o-transform:scale(0.2);transform:scale(0.2)}.ladda-button[data-style=zoom-in] .ladda-label{position:relative;display:inline-block}.ladda-button[data-style=zoom-in][data-loading] .ladda-label{opacity:0;-webkit-transform:scale(2.2);-moz-transform:scale(2.2);-ms-transform:scale(2.2);-o-transform:scale(2.2);transform:scale(2.2)}.ladda-button[data-style=zoom-in][data-loading] .ladda-spinner{opacity:1;margin-left:0;-webkit-transform:none;-moz-transform:none;-ms-transform:none;-o-transform:none;transform:none}.ladda-button[data-style=contract]{overflow:hidden;width:100px}.ladda-button[data-style=contract] .ladda-spinner{left:50%;margin-left:0}.ladda-button[data-style=contract][data-loading]{border-radius:50%;width:52px}.ladda-button[data-style=contract][data-loading] .ladda-label{opacity:0}.ladda-button[data-style=contract][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=contract-overlay]{overflow:hidden;width:100px;box-shadow:0px 0px 0px 2000px transparent}.ladda-button[data-style=contract-overlay] .ladda-spinner{left:50%;margin-left:0}.ladda-button[data-style=contract-overlay][data-loading]{border-radius:50%;width:52px;box-shadow:0px 0px 0px 2000px rgba(0,0,0,0.8)}.ladda-button[data-style=contract-overlay][data-loading] .ladda-label{opacity:0}.ladda-button[data-style=contract-overlay][data-loading] .ladda-spinner{opacity:1}.ladda-button{background:#666;border:0;padding:14px 18px;font-size:18px;cursor:pointer;color:#fff;border-radius:2px;border:1px solid transparent;-webkit-appearance:none;-webkit-font-smoothing:antialiased;-webkit-tap-highlight-color:transparent}.ladda-button:hover{border-color:rgba(0,0,0,0.07);background-color:#888}.ladda-button[data-color=green]{background:#2aca76}.ladda-button[data-color=green]:hover{background-color:#38d683}.ladda-button[data-color=blue]{background:#53b5e6}.ladda-button[data-color=blue]:hover{background-color:#69bfe9}.ladda-button[data-color=red]{background:#ea8557}.ladda-button[data-color=red]:hover{background-color:#ed956e}.ladda-button[data-color=purple]{background:#9973C2}.ladda-button[data-color=purple]:hover{background-color:#a685ca}.ladda-button[data-color=mint]{background:#16a085}.ladda-button[data-color=mint]:hover{background-color:#19b698}.ladda-button[disabled],.ladda-button[data-loading]{border-color:rgba(0,0,0,0.07)}.ladda-button[disabled],.ladda-button[disabled]:hover,.ladda-button[data-loading],.ladda-button[data-loading]:hover{cursor:default;background-color:#999}.ladda-button[data-size=xs]{padding:4px 8px}.ladda-button[data-size=xs] .ladda-label{font-size:0.7em}.ladda-button[data-size=s]{padding:6px 10px}.ladda-button[data-size=s] .ladda-label{font-size:0.9em}.ladda-button[data-size=l] .ladda-label{font-size:1.2em}.ladda-button[data-size=xl] .ladda-label{font-size:1.5em} diff --git a/jumpserver/jumpserver/apps/static/css/style.css b/jumpserver/jumpserver/apps/static/css/style.css index 2457db6a0a20907a2529a28fb0262e7bf3b64f01..70f7f971c87e74b7c9726e2437dff081489077fd 100644 --- a/jumpserver/jumpserver/apps/static/css/style.css +++ b/jumpserver/jumpserver/apps/static/css/style.css @@ -1588,9 +1588,9 @@ table.dataTable thead .sorting_desc_disabled { /*.dataTables_length {*/ /*float: left;*/ /*}*/ -.dataTables_filter label { - margin-right: 5px; -} +/*.dataTables_filter label {*/ +/* margin-right: 5px;*/ +/*}*/ .html5buttons { float: right; } diff --git a/jumpserver/jumpserver/apps/static/js/jumpserver.js b/jumpserver/jumpserver/apps/static/js/jumpserver.js index 879640ec6a83868cd82798c0430bfa41dac6f72e..782182c189f5c0731e679c1ce7e043c1bbb6facd 100644 --- a/jumpserver/jumpserver/apps/static/js/jumpserver.js +++ b/jumpserver/jumpserver/apps/static/js/jumpserver.js @@ -137,14 +137,19 @@ function setAjaxCSRFToken() { }); } -function activeNav() { - var url_array = document.location.pathname.split("/"); - var app = url_array[1]; - var resource = url_array[2]; +function activeNav(prefix) { + var path = document.location.pathname; + if (prefix) { + path = path.replace(prefix, ''); + console.log(path); + } + var urlArray = path.split("/"); + var app = urlArray[1]; + var resource = urlArray[2]; if (app === '') { $('#index').addClass('active'); } else if (app === 'xpack' && resource === 'cloud') { - var item = url_array[3]; + var item = urlArray[3]; $("#" + app).addClass('active'); $('#' + app + ' #' + resource).addClass('active'); $('#' + app + ' #' + resource + ' #' + item + ' a').css('color', '#ffffff'); @@ -153,7 +158,7 @@ function activeNav() { } else { $("#" + app).addClass('active'); $('#' + app + ' #' + resource).addClass('active'); - $('#' + app + ' #' + resource.replaceAll('-', '_')).addClass('active'); + $('#' + app + ' #' + resource.replace(/-/g, '_')).addClass('active'); } } @@ -172,7 +177,7 @@ function formSubmit(props) { */ props = props || {}; var data = props.data || props.form.serializeObject(); - var redirect_to = props.redirect_to; + var redirectTo = props.redirect_to || props.redirectTo; $.ajax({ url: props.url, type: props.method || 'POST', @@ -180,12 +185,8 @@ function formSubmit(props) { contentType: props.content_type || "application/json; charset=utf-8", dataType: props.data_type || "json" }).done(function (data, textState, jqXHR) { - if (redirect_to) { - if (props.message) { - var messages = "ed65330a45559c87345a0eb6ac7812d18d0d8976$[[\"__json_message\"\0540\05425\054\"asdfasdf \\u521b\\u5efa\\u6210\\u529f\"]]" - setCookie("messages", messages) - } - location.href = redirect_to; + if (redirectTo) { + location.href = redirectTo; } else if (typeof props.success === 'function') { return props.success(data, textState, jqXHR); } @@ -249,7 +250,6 @@ function formSubmit(props) { } $('.has-error').get(0).scrollIntoView(); } - }) } @@ -294,6 +294,8 @@ function requestApi(props) { msg = jqXHR.responseJSON.error } else if (jqXHR.responseJSON.msg) { msg = jqXHR.responseJSON.msg + } else if (jqXHR.responseJSON.detail) { + msg = jqXHR.responseJSON.detail } } if (msg === "") { @@ -302,14 +304,14 @@ function requestApi(props) { toastr.error(msg); } if (typeof props.error === 'function') { - return props.error(jqXHR.responseText, jqXHR.status); + return props.error(jqXHR.responseText, jqXHR.responseJSON, jqXHR.status); } }); // return true; } // Sweet Alert for Delete -function objectDelete(obj, name, url, redirectTo) { +function objectDelete(obj, name, url, redirectTo, title, success_message) { function doDelete() { var body = {}; var success = function () { @@ -328,14 +330,14 @@ function objectDelete(obj, name, url, redirectTo) { url: url, body: JSON.stringify(body), method: 'DELETE', - success_message: gettext("Delete the success"), + success_message: success_message || gettext("Delete the success"), success: success, error: fail }); } swal({ - title: gettext('Are you sure about deleting it?'), + title: title || gettext('Are you sure about deleting it?'), text: " [" + name + "] ", type: "warning", showCancelButton: true, @@ -406,11 +408,14 @@ $.fn.serializeObject = function () { }; function makeLabel(data) { - return "" + data[1] + "
    " + return " " + data[1] + "
    " } function parseTableFilter(value) { var cleanValues = []; + if (!value) { + return {} + } var valuesArray = value.split(':'); for (var i=0; i{}}, ...], // uc_html: 'header button', // op_html: 'div.btn-group?', - // paging: true + // paging: true, + // paging_numbers_length: 5; + // hideDefaultDefs: false; // } + var pagingNumbersLength = 5; + if (options.paging_numbers_length){ + pagingNumbersLength = options.paging_numbers_length; + } + setDataTablePagerLength(pagingNumbersLength); var ele = options.ele || $('.dataTable'); var columnDefs = [ { @@ -591,7 +609,8 @@ jumpserver.initServerSideDataTable = function (options) { orderable: false, width: "20px", createdCell: function (td, cellData) { - $(td).html(''.replace('99991937', cellData)); + var data = ''.replace('Id', cellData); + $(td).html(data); } }, { @@ -600,22 +619,30 @@ jumpserver.initServerSideDataTable = function (options) { render: $.fn.dataTable.render.text() } ]; + if (options.hideDefaultDefs) { + columnDefs = []; + } var select_style = options.select_style || 'multi'; columnDefs = options.columnDefs ? options.columnDefs.concat(columnDefs) : columnDefs; var select = { style: select_style, selector: 'td:first-child' }; + var dom = '<"#uc.pull-left"> <"pull-right"<"inline"l> <"#fb.inline"> <"inline"f><"#fa.inline">>' + + 'tr' + + '<"row m-t"<"col-md-8"<"#op.col-md-6"><"col-md-6 text-center"i>><"col-md-4"p>>'; var table = ele.DataTable({ pageLength: options.pageLength || 15, // dom: options.dom || '<"#uc.pull-left">fltr<"row m-t"<"col-md-8"<"#op.col-md-6"><"col-md-6 text-center"i>><"col-md-4"p>>', - dom: options.dom || '<"#uc.pull-left"><"pull-right"<"inline"l><"#fb.inline"><"inline"f><"#fa.inline">>tr<"row m-t"<"col-md-8"<"#op.col-md-6"><"col-md-6 text-center"i>><"col-md-4"p>>', + // dom: options.dom || '<"#uc.pull-left"><"pull-right"<"inline"l><"#fb.inline"><"inline"<"table-filter"f>><"#fa.inline">>tr<"row m-t"<"col-md-8"<"#op.col-md-6"><"col-md-6 text-center"i>><"col-md-4"p>>', + dom: options.dom || dom, order: options.order || [], buttons: [], columnDefs: columnDefs, serverSide: true, processing: true, searchDelay: 800, + oSearch: options.oSearch, ajax: { url: options.ajax_url, error: function (jqXHR, textStatus, errorThrown) { @@ -686,8 +713,14 @@ jumpserver.initServerSideDataTable = function (options) { var rows = table.rows(indexes).data(); $.each(rows, function (id, row) { if (row.id && $.inArray(row.id, table.selected) === -1) { - table.selected.push(row.id); - table.selected_rows.push(row); + if (select.style === 'multi'){ + table.selected.push(row.id); + table.selected_rows.push(row); + } + else{ + table.selected = [row.id]; + table.selected_rows = [row]; + } } }) } @@ -1000,6 +1033,62 @@ function rootNodeAddDom(ztree, callback) { }) } +function APIExportCSV(props) { + /* + { + listUrl: + objectsId: + template: + table: + params: + } + */ + var _listUrl = props.listUrl; + var _objectsId = props.objectsId; + var _template = props.template; + var _table = props.table; + var _params = props.params || {}; + + var tableParams = _table.ajax.params(); + var exportUrl = setUrlParam(_listUrl, 'format', 'csv'); + if (_template) { + exportUrl = setUrlParam(exportUrl, 'template', _template) + } + for (var k in tableParams) { + if (datatableInternalParams.includes(k)) { + continue + } + if (!tableParams[k]) { + continue + } + exportUrl = setUrlParam(exportUrl, k, tableParams[k]) + } + for (var k in _params) { + exportUrl = setUrlParam(exportUrl, k, tableParams[k]) + } + + if (!_objectsId) { + console.log(exportUrl); + window.open(exportUrl); + return + } + + requestApi({ + url: '/api/v1/common/resources/cache/', + data: JSON.stringify({resources: _objectsId}), + method: "POST", + flash_message: false, + success: function (data) { + exportUrl = setUrlParam(exportUrl, 'spm', data.spm); + console.log(exportUrl); + window.open(exportUrl); + }, + failed: function () { + toastr.error(gettext('Export failed')); + } + }); +} + function APIExportData(props) { props = props || {}; $.ajax({ @@ -1049,6 +1138,7 @@ function APIImportData(props) { }, error: function (error) { var data = error.responseJSON; + console.log(data); if (data instanceof Array) { var html = ''; var li = ''; @@ -1109,8 +1199,8 @@ function objectAttrsIsBool(obj, attrs) { attrs.forEach(function (attr) { if (!obj[attr]) { obj[attr] = false - } else if (['on', '1'].includes(obj[attr])) { - obj[attr] = true + } else { + obj[attr] = ['on', '1', 'true', 'True'].includes(obj[attr]); } }) } @@ -1147,7 +1237,7 @@ function toSafeDateISOStr(s) { function toSafeLocalDateStr(d) { var date = safeDate(d); - var date_s = date.toLocaleString(navigator.language, {hour12: false}); + var date_s = date.toLocaleString(getUserLang(), {hour12: false}); return date_s.split("/").join('-') } @@ -1167,7 +1257,7 @@ function getTimeUnits(u) { "m": "分", "s": "秒", }; - if (navigator.language === "zh-CN") { + if (getUserLang() === "zh-CN") { return units[u] } return u @@ -1214,10 +1304,28 @@ function readFile(ref) { return ref } -function nodesSelect2Init(selector, url) { - if (!url) { - url = '/api/v1/assets/nodes/' + + +function select2AjaxInit(option) { + /* + { + selector: + url: , + disabledData: , + displayFormat, + idFormat, } + */ + var selector = option.selector; + var url = option.url; + var disabledData = option.disabledData; + var displayFormat = option.displayFormat || function (data) { + return data.name; + }; + var idFormat = option.idFormat || function (data) { + return data.id; + }; + return $(selector).select2({ closeOnSelect: false, ajax: { @@ -1233,46 +1341,110 @@ function nodesSelect2Init(selector, url) { }, processResults: function (data) { var results = $.map(data.results, function (v, i) { - return {id: v.id, text: v.full_value} + var display = displayFormat(v); + var id = idFormat(v); + var d = {id: id, text: display}; + if (disabledData && disabledData.indexOf(v.id) !== -1) { + d.disabled = true; + } + return d; }); var more = !!data.next; return {results: results, pagination: {"more": more}} } }, }) + } -function usersSelect2Init(selector, url) { +function usersSelect2Init(selector, url, disabledData) { if (!url) { url = '/api/v1/users/users/' } - return $(selector).select2({ - closeOnSelect: false, - ajax: { - url: url, - data: function (params) { - var page = params.page || 1; - var query = { - search: params.term, - offset: (page - 1) * 10, - limit: 10 - }; - return query - }, - processResults: function (data) { - var results = $.map(data.results, function (v, i) { - var display = v.name + '(' + v.username +')'; - return {id: v.id, text: display} - }); - var more = !!data.next; - return {results: results, pagination: {"more": more}} - } - }, - }) + function displayFormat(v) { + return v.name + '(' + v.username +')'; + } + var option = { + url: url, + selector: selector, + disabledData: disabledData, + displayFormat: displayFormat + }; + return select2AjaxInit(option) +} + +function nodesSelect2Init(selector, url, disabledData) { + if (!url) { + url = '/api/v1/assets/nodes/' + } + function displayFormat(v) { + return v.full_value; + } + var option = { + url: url, + selector: selector, + disabledData: disabledData, + displayFormat: displayFormat + }; + return select2AjaxInit(option) } function showCeleryTaskLog(taskId) { var url = '/ops/celery/task/taskId/log/'.replace('taskId', taskId); window.open(url, '', 'width=900,height=600') } + +function getUserLang(){ + let userLangZh = document.cookie.indexOf('django_language=en'); + if (userLangZh === -1){ + return 'zh-CN' + } + else{ + return 'en-US' + } +} + +function initDateRangePicker(selector, options) { + if (!options) { + options = {} + } + var zhLocale = { + format: 'YYYY-MM-DD HH:mm', + separator: ' ~ ', + applyLabel: "应用", + cancelLabel: "取消", + resetLabel: "重置", + daysOfWeek: ["日", "一", "二", "三", "四", "五", "六"],//汉化处理 + monthNames: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"], + }; + var enLocale = { + format: "YYYY-MM-DD HH:mm", + separator: " - ", + applyLabel: "Apply", + cancelLabel: "Cancel", + resetLabel: "Reset", + daysOfWeek: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"], + monthNames: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], + }; + var defaultOption = { + singleDatePicker: true, + showDropdowns: true, + timePicker: true, + timePicker24Hour: true, + autoApply: true, + }; + if (getUserLang() === 'zh-CN') { + defaultOption.locale = zhLocale; + } + else{ + // en-US + defaultOption.locale = enLocale; + } + options = Object.assign(defaultOption, options); + return $(selector).daterangepicker(options); +} + +function reloadPage() { + setTimeout( function () {window.location.reload();}, 300); +} diff --git a/jumpserver/jumpserver/apps/static/js/plugins/ladda/ladda.jquery.min.js b/jumpserver/jumpserver/apps/static/js/plugins/ladda/ladda.jquery.min.js new file mode 100755 index 0000000000000000000000000000000000000000..74fb3ae0359e98c85074e8eed917a93d1e70dd84 --- /dev/null +++ b/jumpserver/jumpserver/apps/static/js/plugins/ladda/ladda.jquery.min.js @@ -0,0 +1,8 @@ +/*! + * Ladda for jQuery + * http://lab.hakim.se/ladda + * MIT licensed + * + * Copyright (C) 2015 Hakim El Hattab, http://hakim.se + */ +!function(a,b){if(void 0===b)return console.error("jQuery required for Ladda.jQuery");var c=[];b=b.extend(b,{ladda:function(b){"stopAll"===b&&a.stopAll()}}),b.fn=b.extend(b.fn,{ladda:function(d){var e=c.slice.call(arguments,1);return"bind"===d?(e.unshift(b(this).selector),a.bind.apply(a,e)):b(this).each(function(){var c,f=b(this);void 0===d?f.data("ladda",a.create(this)):(c=f.data("ladda"),c[d].apply(c,e))}),this}})}(this.Ladda,this.jQuery); \ No newline at end of file diff --git a/jumpserver/jumpserver/apps/static/js/plugins/ladda/ladda.min.js b/jumpserver/jumpserver/apps/static/js/plugins/ladda/ladda.min.js new file mode 100755 index 0000000000000000000000000000000000000000..f7f81ec74ef618773787860f0c2c4f90df77a4c6 --- /dev/null +++ b/jumpserver/jumpserver/apps/static/js/plugins/ladda/ladda.min.js @@ -0,0 +1,8 @@ +/*! + * Ladda 1.0.0 (2016-03-08, 09:31) + * http://lab.hakim.se/ladda + * MIT licensed + * + * Copyright (C) 2016 Hakim El Hattab, http://hakim.se + */ +!function(a,b){"object"==typeof exports?module.exports=b(require("spin.js")):"function"==typeof define&&define.amd?define(["spin"],b):a.Ladda=b(a.Spinner)}(this,function(a){"use strict";function b(a){if("undefined"==typeof a)return void console.warn("Ladda button target must be defined.");if(/ladda-button/i.test(a.className)||(a.className+=" ladda-button"),a.hasAttribute("data-style")||a.setAttribute("data-style","expand-right"),!a.querySelector(".ladda-label")){var b=document.createElement("span");b.className="ladda-label",i(a,b)}var c,d=a.querySelector(".ladda-spinner");d||(d=document.createElement("span"),d.className="ladda-spinner"),a.appendChild(d);var e,f={start:function(){return c||(c=g(a)),a.setAttribute("disabled",""),a.setAttribute("data-loading",""),clearTimeout(e),c.spin(d),this.setProgress(0),this},startAfter:function(a){return clearTimeout(e),e=setTimeout(function(){f.start()},a),this},stop:function(){return a.removeAttribute("disabled"),a.removeAttribute("data-loading"),clearTimeout(e),c&&(e=setTimeout(function(){c.stop()},1e3)),this},toggle:function(){return this.isLoading()?this.stop():this.start(),this},setProgress:function(b){b=Math.max(Math.min(b,1),0);var c=a.querySelector(".ladda-progress");0===b&&c&&c.parentNode?c.parentNode.removeChild(c):(c||(c=document.createElement("div"),c.className="ladda-progress",a.appendChild(c)),c.style.width=(b||0)*a.offsetWidth+"px")},enable:function(){return this.stop(),this},disable:function(){return this.stop(),a.setAttribute("disabled",""),this},isLoading:function(){return a.hasAttribute("data-loading")},remove:function(){clearTimeout(e),a.removeAttribute("disabled",""),a.removeAttribute("data-loading",""),c&&(c.stop(),c=null);for(var b=0,d=j.length;d>b;b++)if(f===j[b]){j.splice(b,1);break}}};return j.push(f),f}function c(a,b){for(;a.parentNode&&a.tagName!==b;)a=a.parentNode;return b===a.tagName?a:void 0}function d(a){for(var b=["input","textarea","select"],c=[],d=0;dg;g++)!function(){var a=f[g];if("function"==typeof a.addEventListener){var h=b(a),i=-1;a.addEventListener("click",function(b){var f=!0,g=c(a,"FORM");if("undefined"!=typeof g)if("function"==typeof g.checkValidity)f=g.checkValidity();else for(var j=d(g),k=0;ka;a++)j[a].stop()}function g(b){var c,d,e=b.offsetHeight;0===e&&(e=parseFloat(window.getComputedStyle(b).height)),e>32&&(e*=.8),b.hasAttribute("data-spinner-size")&&(e=parseInt(b.getAttribute("data-spinner-size"),10)),b.hasAttribute("data-spinner-color")&&(c=b.getAttribute("data-spinner-color")),b.hasAttribute("data-spinner-lines")&&(d=parseInt(b.getAttribute("data-spinner-lines"),10));var f=.2*e,g=.6*f,h=7>f?2:3;return new a({color:c||"#fff",lines:d||12,radius:f,length:g,width:h,zIndex:"auto",top:"auto",left:"auto",className:""})}function h(a){for(var b=[],c=0;cb;b++)a.appendChild(arguments[b]);return a}function c(a,b,c,d){var e=["opacity",b,~~(100*a),c,d].join("-"),f=.01+c/d*100,g=Math.max(1-(1-a)/b*(100-f),a),h=j.substring(0,j.indexOf("Animation")).toLowerCase(),i=h&&"-"+h+"-"||"";return l[e]||(m.insertRule("@"+i+"keyframes "+e+"{0%{opacity:"+g+"}"+f+"%{opacity:"+a+"}"+(f+.01)+"%{opacity:1}"+(f+b)%100+"%{opacity:"+a+"}100%{opacity:"+g+"}}",m.cssRules.length),l[e]=1),e}function d(a,b){var c,d,e=a.style;for(b=b.charAt(0).toUpperCase()+b.slice(1),d=0;d',c)}m.addRule(".spin-vml","behavior:url(#default#VML)"),h.prototype.lines=function(a,d){function f(){return e(c("group",{coordsize:k+" "+k,coordorigin:-j+" "+-j}),{width:k,height:k})}function h(a,h,i){b(m,b(e(f(),{rotation:360/d.lines*a+"deg",left:~~h}),b(e(c("roundrect",{arcsize:d.corners}),{width:j,height:d.width,left:d.radius,top:-d.width>>1,filter:i}),c("fill",{color:g(d.color,a),opacity:d.opacity}),c("stroke",{opacity:0}))))}var i,j=d.length+d.width,k=2*j,l=2*-(d.width+d.length)+"px",m=e(f(),{position:"absolute",top:l,left:l});if(d.shadow)for(i=1;i<=d.lines;i++)h(i,-2,"progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)");for(i=1;i<=d.lines;i++)h(i);return b(a,m)},h.prototype.opacity=function(a,b,c,d){var e=a.firstChild;d=d.shadow&&d.lines||0,e&&b+d>1)+"px"})}for(var i,k=0,l=(f.lines-1)*(1-f.direction)/2;k +
    +
    + {% include 'assets/_node_tree.html' %} +
    +
    +
    +
    + +
    +
    +
    + {% block table_container %} + + + + {% block table_head %} {% endblock %} + + + + {% block table_body %} {% endblock %} + +
    + {% endblock %} +
    +
    +
    +
    + +{% endblock %} diff --git a/jumpserver/jumpserver/apps/templates/_base_create_update.html b/jumpserver/jumpserver/apps/templates/_base_create_update.html index be3813804a7278c1baa5991f6ee6c707cacc8023..d206d40b27babcb57866799d2514fab20eefd396 100644 --- a/jumpserver/jumpserver/apps/templates/_base_create_update.html +++ b/jumpserver/jumpserver/apps/templates/_base_create_update.html @@ -3,8 +3,6 @@ {% load static %} {% load bootstrap3 %} {% block custom_head_css_js %} - - {% block custom_head_css_js_create %} {% endblock %} {% endblock %} diff --git a/jumpserver/jumpserver/apps/templates/_base_list.html b/jumpserver/jumpserver/apps/templates/_base_list.html index c5314af4e59d9f60802c16cbd31656fa03112d83..9759081bd0f44a3aeda4a9f75ff9e4f102c8b4ee 100644 --- a/jumpserver/jumpserver/apps/templates/_base_list.html +++ b/jumpserver/jumpserver/apps/templates/_base_list.html @@ -1,10 +1,6 @@ {% extends 'base.html' %} {% load static %} {% load i18n %} -{% block custom_head_css_js %} - - -{% endblock %} {% block content %}
    diff --git a/jumpserver/jumpserver/apps/templates/_base_only_content.html b/jumpserver/jumpserver/apps/templates/_base_only_content.html new file mode 100644 index 0000000000000000000000000000000000000000..1e6a16c8444b7c73e05fa4d83f83de1a828f374d --- /dev/null +++ b/jumpserver/jumpserver/apps/templates/_base_only_content.html @@ -0,0 +1,47 @@ +{% load static %} +{% load i18n %} + + + + + + + + {% block html_title %}{% endblock %} + + {% include '_head_css_js.html' %} + + + + + {% block custom_head_css_js %} {% endblock %} + + + +
    +
    +
    +
    + +

    {% block title %}{% endblock %}

    +

    + {% block content %} {% endblock %} +
    +
    +
    +
    +
    +
    + {% include '_copyright.html' %} +
    +
    +
    + +{% block custom_foot_js %} {% endblock %} + diff --git a/jumpserver/jumpserver/apps/templates/_csv_import_export.html b/jumpserver/jumpserver/apps/templates/_csv_import_export.html new file mode 100644 index 0000000000000000000000000000000000000000..d22c947b4eebf9552078b3b1c326291813a4336c --- /dev/null +++ b/jumpserver/jumpserver/apps/templates/_csv_import_export.html @@ -0,0 +1,62 @@ +{% load i18n %} + +{% include '_csv_import_modal.html' %} +{% include '_csv_update_modal.html' %} + + diff --git a/jumpserver/jumpserver/apps/templates/_csv_import_modal.html b/jumpserver/jumpserver/apps/templates/_csv_import_modal.html new file mode 100644 index 0000000000000000000000000000000000000000..4925793d0c19a0e33526ea1d0c283c71da0776cb --- /dev/null +++ b/jumpserver/jumpserver/apps/templates/_csv_import_modal.html @@ -0,0 +1,52 @@ +{% extends '_modal.html' %} +{% load i18n %} + +{% block modal_id %}csv_import_modal{% endblock %} +{% block modal_title%}csv {% trans 'Import' %}{% endblock %} +{% block modal_confirm_id %}btn_csv_import_confirm{% endblock %} + +{% block modal_body %} +
    + {% csrf_token %} +
    + + {% trans 'Download the import template' %} +
    + +
    + + +
    +
    + +
    +

    +

    +

    +

    +
    + + +{% endblock %} + + diff --git a/jumpserver/jumpserver/apps/templates/_csv_update_modal.html b/jumpserver/jumpserver/apps/templates/_csv_update_modal.html new file mode 100644 index 0000000000000000000000000000000000000000..c4c31abdaac7d4e04c14d6ba229f9abe22fd31d9 --- /dev/null +++ b/jumpserver/jumpserver/apps/templates/_csv_update_modal.html @@ -0,0 +1,54 @@ +{% extends '_modal.html' %} +{% load i18n %} + +{% block modal_id %}csv_update_modal{% endblock %} +{% block modal_confirm_id %}btn_csv_update_confirm{% endblock %} +{% block modal_title%}csv {% trans 'Update' %}{% endblock %} + +{% block modal_body %} +
    + {% csrf_token %} +
    + + {% trans 'Download the update template' %} +
    + +
    + + +
    +
    + +
    +

    +

    +

    +

    +
    + + +{% endblock %} diff --git a/jumpserver/jumpserver/apps/templates/_filter_dropdown.html b/jumpserver/jumpserver/apps/templates/_filter_dropdown.html new file mode 100644 index 0000000000000000000000000000000000000000..3fff427bc42a92b2331d7e7aa38fbf4ba5a3443d --- /dev/null +++ b/jumpserver/jumpserver/apps/templates/_filter_dropdown.html @@ -0,0 +1,86 @@ + + + + diff --git a/jumpserver/jumpserver/apps/templates/_foot_js.html b/jumpserver/jumpserver/apps/templates/_foot_js.html index f9cfbba591f2dd5cac060ebab1a81dbb1b84c99b..b140e78624585777ef53054aa7723682ce1e1b12 100644 --- a/jumpserver/jumpserver/apps/templates/_foot_js.html +++ b/jumpserver/jumpserver/apps/templates/_foot_js.html @@ -7,14 +7,17 @@ - + + + diff --git a/jumpserver/jumpserver/apps/templates/_head_css_js.html b/jumpserver/jumpserver/apps/templates/_head_css_js.html index 95dc88444360957d1910dbfa8e82192e81e05759..b4fbcd47b21122b491635fd9ad7a9e01520edb22 100644 --- a/jumpserver/jumpserver/apps/templates/_head_css_js.html +++ b/jumpserver/jumpserver/apps/templates/_head_css_js.html @@ -12,4 +12,6 @@ - \ No newline at end of file + + + diff --git a/jumpserver/jumpserver/apps/templates/_import_modal.html b/jumpserver/jumpserver/apps/templates/_import_modal.html deleted file mode 100644 index 9211bdcb932ad93fc3f3e2d92e348ae962204352..0000000000000000000000000000000000000000 --- a/jumpserver/jumpserver/apps/templates/_import_modal.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends '_modal.html' %} -{% load i18n %} - -{% block modal_id %}import_modal{% endblock %} - -{% block modal_confirm_id %}btn_import_confirm{% endblock %} - -{% block modal_body %} -
    - {% csrf_token %} -
    - - {% trans 'Download the import template' %} -
    - -
    - - -
    -
    - -
    -

    -

    -

    -

    -
    -{% endblock %} diff --git a/jumpserver/jumpserver/apps/templates/_nav.html b/jumpserver/jumpserver/apps/templates/_nav.html index 5690c961d3545826e8eca03641c27e1f3ccf7360..e517b901eeb001eb368d6bfce9c44c7b120dc163 100644 --- a/jumpserver/jumpserver/apps/templates/_nav.html +++ b/jumpserver/jumpserver/apps/templates/_nav.html @@ -45,19 +45,25 @@
  • {% trans 'System user' %}
  • {% trans 'Labels' %}
  • {% trans 'Command filters' %}
  • + {% if request.user.is_superuser %} +
  • {% trans 'Platform list' %}
  • + {% endif %} {% endif %} {# Applications #} -{% if request.user.can_admin_current_org and LICENSE_VALID %} +{% if request.user.can_admin_current_org %}
  • {% trans 'Applications' %}
  • {% endif %} @@ -76,6 +82,9 @@ {% trans 'RemoteApp' %} {% endif %} +
  • + {% trans 'DatabaseApp' %} +
  • {% endif %} @@ -113,7 +122,7 @@
    +{% endblock %} +{% block content_bottom_left %}{% endblock %} +{% block custom_foot_js %} + +{% endblock %} + diff --git a/jumpserver/jumpserver/apps/tickets/tests.py b/jumpserver/jumpserver/apps/tickets/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/jumpserver/jumpserver/apps/tickets/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/jumpserver/jumpserver/apps/tickets/urls/__init__.py b/jumpserver/jumpserver/apps/tickets/urls/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ec51c5a2b9dd623073fa752065a5b5a615780c7f --- /dev/null +++ b/jumpserver/jumpserver/apps/tickets/urls/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# diff --git a/jumpserver/jumpserver/apps/tickets/urls/api_urls.py b/jumpserver/jumpserver/apps/tickets/urls/api_urls.py new file mode 100644 index 0000000000000000000000000000000000000000..33cb5a21654533115869ad775d499ba4d84b166e --- /dev/null +++ b/jumpserver/jumpserver/apps/tickets/urls/api_urls.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# +from rest_framework_bulk.routes import BulkRouter + +from .. import api + +app_name = 'tickets' +router = BulkRouter() + +router.register('tickets', api.TicketViewSet, 'ticket') +router.register('tickets/(?P[0-9a-zA-Z\-]{36})/comments', api.TicketCommentViewSet, 'ticket-comment') + + +urlpatterns = [ +] + +urlpatterns += router.urls diff --git a/jumpserver/jumpserver/apps/tickets/urls/views_urls.py b/jumpserver/jumpserver/apps/tickets/urls/views_urls.py new file mode 100644 index 0000000000000000000000000000000000000000..46e15437e10bc2df96651edd28065795e2680540 --- /dev/null +++ b/jumpserver/jumpserver/apps/tickets/urls/views_urls.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# +from django.urls import path +from .. import views + +app_name = 'tickets' + +urlpatterns = [ + path('tickets/', views.TicketListView.as_view(), name='ticket-list'), + path('tickets//', views.TicketDetailView.as_view(), name='ticket-detail'), +] diff --git a/jumpserver/jumpserver/apps/tickets/utils.py b/jumpserver/jumpserver/apps/tickets/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..13727a77d0499d9d43b2baad1fb8145caec2a38e --- /dev/null +++ b/jumpserver/jumpserver/apps/tickets/utils.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# +from django.conf import settings +from django.utils.translation import ugettext as _ + +from common.utils import get_logger, reverse +from common.tasks import send_mail_async + +logger = get_logger(__name__) + + +def send_new_ticket_mail_to_assignees(ticket, assignees): + recipient_list = [user.email for user in assignees] + user = ticket.user + if not recipient_list: + logger.error("Ticket not has assignees: {}".format(ticket.id)) + return + subject = '{}: {}'.format(_("New ticket"), ticket.title) + detail_url = reverse('tickets:ticket-detail', + kwargs={'pk': ticket.id}, external=True) + message = _(""" +
    +

    Your has a new ticket

    +
    + {body} +
    + click here to review +
    +
    + """).format(body=ticket.body, user=user, url=detail_url) + send_mail_async.delay(subject, message, recipient_list, html_message=message) + + +def send_ticket_action_mail_to_user(ticket): + if not ticket.user: + logger.error("Ticket not has user: {}".format(ticket.id)) + return + user = ticket.user + recipient_list = [user.email] + subject = '{}: {}'.format(_("Ticket has been reply"), ticket.title) + message = _(""" +
    +

    Your ticket has been replay

    +
    + Title: {ticket.title} +
    + Assignee: {ticket.assignee_display} +
    + Status: {ticket.status_display} +
    +
    +
    + """).format(ticket=ticket) + send_mail_async.delay(subject, message, recipient_list, html_message=message) diff --git a/jumpserver/jumpserver/apps/tickets/views.py b/jumpserver/jumpserver/apps/tickets/views.py new file mode 100644 index 0000000000000000000000000000000000000000..93b4aca2fa8f45af34751aa6f38cd53a06c740d9 --- /dev/null +++ b/jumpserver/jumpserver/apps/tickets/views.py @@ -0,0 +1,41 @@ +from django.views.generic import TemplateView, DetailView +from django.utils.translation import ugettext as _ + +from common.permissions import PermissionsMixin, IsValidUser +from .models import Ticket +from . import mixins + + +class TicketListView(PermissionsMixin, TemplateView): + template_name = 'tickets/ticket_list.html' + permission_classes = (IsValidUser,) + + def get_context_data(self, **kwargs): + assign = self.request.GET.get('assign', '0') == '1' + context = super().get_context_data(**kwargs) + assigned_open_count = Ticket.get_assigned_tickets(self.request.user)\ + .filter(status=Ticket.STATUS_OPEN).count() + context.update({ + 'app': _("Tickets"), + 'action': _("Ticket list"), + 'assign': assign, + 'assigned_open_count': assigned_open_count + }) + return context + + +class TicketDetailView(PermissionsMixin, mixins.TicketMixin, DetailView): + template_name = 'tickets/ticket_detail.html' + permission_classes = (IsValidUser,) + queryset = Ticket.objects.all() + + def get_context_data(self, **kwargs): + ticket = self.get_object() + has_action_perm = ticket.is_assignee(self.request.user) + context = super().get_context_data(**kwargs) + context.update({ + 'app': _("Tickets"), + 'action': _("Ticket detail"), + 'has_action_perm': has_action_perm, + }) + return context diff --git a/jumpserver/jumpserver/apps/users/api/__init__.py b/jumpserver/jumpserver/apps/users/api/__init__.py index 97e1f1088567b02f8066707906255236578bf3fb..965337c422b690c80d221016e90c587252e717db 100644 --- a/jumpserver/jumpserver/apps/users/api/__init__.py +++ b/jumpserver/jumpserver/apps/users/api/__init__.py @@ -3,3 +3,4 @@ from .user import * from .group import * +from .relation import * diff --git a/jumpserver/jumpserver/apps/users/api/group.py b/jumpserver/jumpserver/apps/users/api/group.py index eb00ea220e3fc208328b8e3e0a08e2390e009452..860ca36b40b34b9821acccbfb15a0cd13a63128d 100644 --- a/jumpserver/jumpserver/apps/users/api/group.py +++ b/jumpserver/jumpserver/apps/users/api/group.py @@ -1,35 +1,19 @@ # -*- coding: utf-8 -*- # -from ..serializers import ( - UserGroupSerializer, - UserGroupListSerializer, - UserGroupUpdateMemberSerializer, -) +from ..serializers import UserGroupSerializer from ..models import UserGroup from orgs.mixins.api import OrgBulkModelViewSet -from orgs.mixins import generics from common.permissions import IsOrgAdmin -__all__ = ['UserGroupViewSet', 'UserGroupUpdateUserApi'] +__all__ = ['UserGroupViewSet'] class UserGroupViewSet(OrgBulkModelViewSet): model = UserGroup filter_fields = ("name",) search_fields = filter_fields - serializer_class = UserGroupSerializer permission_classes = (IsOrgAdmin,) + serializer_class = UserGroupSerializer - def get_serializer_class(self): - if self.action in ("list", 'retrieve') and \ - self.request.query_params.get("display"): - return UserGroupListSerializer - return self.serializer_class - - -class UserGroupUpdateUserApi(generics.RetrieveUpdateAPIView): - model = UserGroup - serializer_class = UserGroupUpdateMemberSerializer - permission_classes = (IsOrgAdmin,) diff --git a/jumpserver/jumpserver/apps/users/api/relation.py b/jumpserver/jumpserver/apps/users/api/relation.py new file mode 100644 index 0000000000000000000000000000000000000000..fbab92ee9652e47c34e01c391418f77b268d0053 --- /dev/null +++ b/jumpserver/jumpserver/apps/users/api/relation.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# + +from rest_framework_bulk import BulkModelViewSet +from django.db.models import F + +from common.permissions import IsOrgAdmin +from .. import serializers +from ..models import User + +__all__ = ['UserUserGroupRelationViewSet'] + + +class UserUserGroupRelationViewSet(BulkModelViewSet): + filter_fields = ('user', 'usergroup') + search_fields = filter_fields + serializer_class = serializers.UserUserGroupRelationSerializer + permission_classes = (IsOrgAdmin,) + + def get_queryset(self): + queryset = User.groups.through.objects.all()\ + .annotate(user_display=F('user__name'))\ + .annotate(usergroup_display=F('usergroup__name')) + return queryset + + def allow_bulk_destroy(self, qs, filtered): + if filtered.count() != 1: + return False + else: + return True diff --git a/jumpserver/jumpserver/apps/users/api/user.py b/jumpserver/jumpserver/apps/users/api/user.py index 4545f629b55eb8b89b40bc7a16f03b313e0f0fa9..98dcbd91ca26a08c14bdd611d1185f37fd05f878 100644 --- a/jumpserver/jumpserver/apps/users/api/user.py +++ b/jumpserver/jumpserver/apps/users/api/user.py @@ -24,7 +24,7 @@ from ..signals import post_user_create logger = get_logger(__name__) __all__ = [ - 'UserViewSet', 'UserChangePasswordApi', 'UserUpdateGroupApi', + 'UserViewSet', 'UserChangePasswordApi', 'UserResetPasswordApi', 'UserResetPKApi', 'UserUpdatePKApi', 'UserUnblockPKApi', 'UserProfileApi', 'UserResetOTPApi', ] @@ -39,7 +39,10 @@ class UserQuerysetMixin: class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet): filter_fields = ('username', 'email', 'name', 'id') search_fields = filter_fields - serializer_class = serializers.UserSerializer + serializer_classes = { + 'default': serializers.UserSerializer, + 'display': serializers.UserDisplaySerializer + } permission_classes = (IsOrgAdmin, CanUpdateDeleteUser) def get_queryset(self): @@ -66,6 +69,12 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet): self.permission_classes = (IsSuperUser,) return super().get_permissions() + def perform_destroy(self, instance): + if current_org.is_real(): + instance.remove() + else: + return super().perform_destroy(instance) + def perform_bulk_destroy(self, objects): for obj in objects: self.check_object_permissions(self.request, obj) @@ -92,11 +101,6 @@ class UserChangePasswordApi(UserQuerysetMixin, generics.RetrieveUpdateAPIView): user.save() -class UserUpdateGroupApi(UserQuerysetMixin, generics.RetrieveUpdateAPIView): - serializer_class = serializers.UserUpdateGroupSerializer - permission_classes = (IsOrgAdmin,) - - class UserResetPasswordApi(UserQuerysetMixin, generics.UpdateAPIView): queryset = User.objects.all() serializer_class = serializers.UserSerializer @@ -172,8 +176,7 @@ class UserResetOTPApi(UserQuerysetMixin, generics.RetrieveAPIView): if user == request.user: msg = _("Could not reset self otp, use profile reset instead") return Response({"error": msg}, status=401) - if user.otp_enabled and user.otp_secret_key: - user.otp_secret_key = '' + if user.mfa_enabled: + user.reset_mfa() user.save() - logout(request) return Response({"msg": "success"}) diff --git a/jumpserver/jumpserver/apps/users/forms/__init__.py b/jumpserver/jumpserver/apps/users/forms/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b4d5d888bd8629cfd57ca31098064661c7cbac5 --- /dev/null +++ b/jumpserver/jumpserver/apps/users/forms/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# +from .user import * +from .group import * +from .profile import * diff --git a/jumpserver/jumpserver/apps/users/forms/group.py b/jumpserver/jumpserver/apps/users/forms/group.py new file mode 100644 index 0000000000000000000000000000000000000000..8d026a777ae820829cc5b421a009340cde917bd1 --- /dev/null +++ b/jumpserver/jumpserver/apps/users/forms/group.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# +from django import forms +from django.utils.translation import gettext_lazy as _ + +from orgs.mixins.forms import OrgModelForm +from ..models import User, UserGroup + +__all__ = ['UserGroupForm'] + + +class UserGroupForm(OrgModelForm): + users = forms.ModelMultipleChoiceField( + queryset=User.objects.none(), + label=_("User"), + widget=forms.SelectMultiple( + attrs={ + 'class': 'users-select2', + 'data-placeholder': _('Select users') + } + ), + required=False, + ) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.set_fields_queryset() + + def set_fields_queryset(self): + users_field = self.fields.get('users') + if self.instance: + users_field.initial = self.instance.users.all() + users_field.queryset = self.instance.users.all() + else: + users_field.queryset = User.objects.none() + + def save(self, commit=True): + raise Exception("Save by restful api") + + class Meta: + model = UserGroup + fields = [ + 'name', 'users', 'comment', + ] diff --git a/jumpserver/jumpserver/apps/users/forms/profile.py b/jumpserver/jumpserver/apps/users/forms/profile.py new file mode 100644 index 0000000000000000000000000000000000000000..bd1047733499921970567f3d8d4a9a8829f05ef0 --- /dev/null +++ b/jumpserver/jumpserver/apps/users/forms/profile.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +# +from django import forms +from django.utils.translation import gettext_lazy as _ +from captcha.fields import CaptchaField + +from common.utils import validate_ssh_public_key +from ..models import User + + +__all__ = [ + 'UserProfileForm', 'UserMFAForm', 'UserFirstLoginFinishForm', + 'UserPasswordForm', 'UserPublicKeyForm', 'FileForm', + 'UserTokenResetPasswordForm', 'UserForgotPasswordForm', +] + + +class UserProfileForm(forms.ModelForm): + username = forms.CharField(disabled=True, label=_("Username")) + name = forms.CharField(disabled=True, label=_("Name")) + email = forms.CharField(disabled=True) + + class Meta: + model = User + fields = [ + 'username', 'name', 'email', + 'wechat', 'phone', + ] + + +UserProfileForm.verbose_name = _("Profile") + + +class UserMFAForm(forms.ModelForm): + + mfa_description = _( + 'When enabled, ' + 'you will enter the MFA binding process the next time you log in. ' + 'you can also directly bind in ' + '"personal information -> quick modification -> change MFA Settings"!') + + class Meta: + model = User + fields = ['mfa_level'] + widgets = {'mfa_level': forms.RadioSelect()} + help_texts = { + 'mfa_level': _('* Enable MFA authentication ' + 'to make the account more secure.'), + } + + +UserMFAForm.verbose_name = _("MFA") + + +class UserFirstLoginFinishForm(forms.Form): + finish_description = _( + 'In order to protect you and your company, ' + 'please keep your account, ' + 'password and key sensitive information properly. ' + '(for example: setting complex password, enabling MFA authentication)' + ) + + +UserFirstLoginFinishForm.verbose_name = _("Finish") + + +class UserTokenResetPasswordForm(forms.Form): + new_password = forms.CharField( + min_length=5, max_length=128, + widget=forms.PasswordInput, + label=_("New password") + ) + confirm_password = forms.CharField( + min_length=5, max_length=128, + widget=forms.PasswordInput, + label=_("Confirm password") + ) + + def clean_confirm_password(self): + new_password = self.cleaned_data['new_password'] + confirm_password = self.cleaned_data['confirm_password'] + + if new_password != confirm_password: + raise forms.ValidationError(_('Password does not match')) + return confirm_password + + +class UserForgotPasswordForm(forms.Form): + email = forms.EmailField(label=_("Email")) + captcha = CaptchaField(label=_("Captcha")) + + +class UserPasswordForm(UserTokenResetPasswordForm): + old_password = forms.CharField( + max_length=128, widget=forms.PasswordInput, + label=_("Old password") + ) + + def __init__(self, *args, **kwargs): + self.instance = kwargs.pop('instance') + super().__init__(*args, **kwargs) + + def clean_old_password(self): + old_password = self.cleaned_data['old_password'] + if not self.instance.check_password(old_password): + raise forms.ValidationError(_('Old password error')) + return old_password + + def save(self): + password = self.cleaned_data['new_password'] + self.instance.reset_password(new_password=password) + return self.instance + + +class UserPublicKeyForm(forms.Form): + pubkey_description = _('Automatically configure and download the SSH key') + public_key = forms.CharField( + label=_('ssh public key'), max_length=5000, required=False, + widget=forms.Textarea(attrs={'placeholder': _('ssh-rsa AAAA...')}), + help_text=_('Paste your id_rsa.pub here.') + ) + + def __init__(self, *args, **kwargs): + if 'instance' in kwargs: + self.instance = kwargs.pop('instance') + else: + self.instance = None + super().__init__(*args, **kwargs) + + def clean_public_key(self): + public_key = self.cleaned_data['public_key'] + if self.instance.public_key and public_key == self.instance.public_key: + msg = _('Public key should not be the same as your old one.') + raise forms.ValidationError(msg) + + if public_key and not validate_ssh_public_key(public_key): + raise forms.ValidationError(_('Not a valid ssh public key')) + return public_key + + def save(self): + public_key = self.cleaned_data['public_key'] + if public_key: + self.instance.public_key = public_key + self.instance.save() + return self.instance + + +UserPublicKeyForm.verbose_name = _("Public key") + + +class FileForm(forms.Form): + file = forms.FileField() diff --git a/jumpserver/jumpserver/apps/users/forms.py b/jumpserver/jumpserver/apps/users/forms/user.py similarity index 51% rename from jumpserver/jumpserver/apps/users/forms.py rename to jumpserver/jumpserver/apps/users/forms/user.py index 649f66ab9bf260d630d2aabd3c8bab61409bed50..a58e1fef1874d190f7620aa8f138ea1608700bce 100644 --- a/jumpserver/jumpserver/apps/users/forms.py +++ b/jumpserver/jumpserver/apps/users/forms/user.py @@ -1,39 +1,19 @@ -# ~*~ coding: utf-8 ~*~ from django import forms from django.utils.translation import gettext_lazy as _ -from django.conf import settings from common.utils import validate_ssh_public_key from orgs.mixins.forms import OrgModelForm -from .models import User, UserGroup -from .utils import check_password_rules, get_current_org_members +from ..models import User +from ..utils import ( + check_password_rules, get_current_org_members, get_source_choices +) -class UserCheckPasswordForm(forms.Form): - username = forms.CharField(label=_('Username'), max_length=100) - password = forms.CharField( - label=_('Password'), widget=forms.PasswordInput, - max_length=128, strip=False - ) - - -class UserCheckOtpCodeForm(forms.Form): - otp_code = forms.CharField(label=_('MFA code'), max_length=6) - - -def get_source_choices(): - choices_all = dict(User.SOURCE_CHOICES) - choices = [ - (User.SOURCE_LOCAL, choices_all[User.SOURCE_LOCAL]), - ] - if settings.AUTH_LDAP: - choices.append((User.SOURCE_LDAP, choices_all[User.SOURCE_LDAP])) - if settings.AUTH_OPENID: - choices.append((User.SOURCE_OPENID, choices_all[User.SOURCE_OPENID])) - if settings.AUTH_RADIUS: - choices.append((User.SOURCE_RADIUS, choices_all[User.SOURCE_RADIUS])) - return choices +__all__ = [ + 'UserCreateForm', 'UserUpdateForm', 'UserBulkUpdateForm', + 'UserCheckOtpCodeForm', 'UserCheckPasswordForm' +] class UserCreateUpdateFormMixin(OrgModelForm): @@ -61,10 +41,10 @@ class UserCreateUpdateFormMixin(OrgModelForm): fields = [ 'username', 'name', 'email', 'groups', 'wechat', 'source', 'phone', 'role', 'date_expired', - 'comment', 'otp_level' + 'comment', 'mfa_level' ] widgets = { - 'otp_level': forms.RadioSelect(), + 'mfa_level': forms.RadioSelect(), 'groups': forms.SelectMultiple( attrs={ 'class': 'select2', @@ -126,13 +106,13 @@ class UserCreateUpdateFormMixin(OrgModelForm): def save(self, commit=True): password = self.cleaned_data.get('password') - otp_level = self.cleaned_data.get('otp_level') + mfa_level = self.cleaned_data.get('mfa_level') public_key = self.cleaned_data.get('public_key') user = super().save(commit=commit) if password: user.reset_password(password) - if otp_level: - user.otp_level = otp_level + if mfa_level: + user.mfa_level = mfa_level user.save() if public_key: user.public_key = public_key @@ -157,131 +137,6 @@ class UserUpdateForm(UserCreateUpdateFormMixin): pass -class UserProfileForm(forms.ModelForm): - username = forms.CharField(disabled=True) - name = forms.CharField(disabled=True) - email = forms.CharField(disabled=True) - - class Meta: - model = User - fields = [ - 'username', 'name', 'email', - 'wechat', 'phone', - ] - - -UserProfileForm.verbose_name = _("Profile") - - -class UserMFAForm(forms.ModelForm): - - mfa_description = _( - 'When enabled, ' - 'you will enter the MFA binding process the next time you log in. ' - 'you can also directly bind in ' - '"personal information -> quick modification -> change MFA Settings"!') - - class Meta: - model = User - fields = ['otp_level'] - widgets = {'otp_level': forms.RadioSelect()} - help_texts = { - 'otp_level': _('* Enable MFA authentication ' - 'to make the account more secure.'), - } - - -UserMFAForm.verbose_name = _("MFA") - - -class UserFirstLoginFinishForm(forms.Form): - finish_description = _( - 'In order to protect you and your company, ' - 'please keep your account, ' - 'password and key sensitive information properly. ' - '(for example: setting complex password, enabling MFA authentication)' - ) - - -UserFirstLoginFinishForm.verbose_name = _("Finish") - - -class UserPasswordForm(forms.Form): - old_password = forms.CharField( - max_length=128, widget=forms.PasswordInput, - label=_("Old password") - ) - new_password = forms.CharField( - min_length=5, max_length=128, - widget=forms.PasswordInput, - label=_("New password") - ) - confirm_password = forms.CharField( - min_length=5, max_length=128, - widget=forms.PasswordInput, - label=_("Confirm password") - ) - - def __init__(self, *args, **kwargs): - self.instance = kwargs.pop('instance') - super().__init__(*args, **kwargs) - - def clean_old_password(self): - old_password = self.cleaned_data['old_password'] - if not self.instance.check_password(old_password): - raise forms.ValidationError(_('Old password error')) - return old_password - - def clean_confirm_password(self): - new_password = self.cleaned_data['new_password'] - confirm_password = self.cleaned_data['confirm_password'] - - if new_password != confirm_password: - raise forms.ValidationError(_('Password does not match')) - return confirm_password - - def save(self): - password = self.cleaned_data['new_password'] - self.instance.reset_password(new_password=password) - return self.instance - - -class UserPublicKeyForm(forms.Form): - pubkey_description = _('Automatically configure and download the SSH key') - public_key = forms.CharField( - label=_('ssh public key'), max_length=5000, required=False, - widget=forms.Textarea(attrs={'placeholder': _('ssh-rsa AAAA...')}), - help_text=_('Paste your id_rsa.pub here.') - ) - - def __init__(self, *args, **kwargs): - if 'instance' in kwargs: - self.instance = kwargs.pop('instance') - else: - self.instance = None - super().__init__(*args, **kwargs) - - def clean_public_key(self): - public_key = self.cleaned_data['public_key'] - if self.instance.public_key and public_key == self.instance.public_key: - msg = _('Public key should not be the same as your old one.') - raise forms.ValidationError(msg) - - if public_key and not validate_ssh_public_key(public_key): - raise forms.ValidationError(_('Not a valid ssh public key')) - return public_key - - def save(self): - public_key = self.cleaned_data['public_key'] - if public_key: - self.instance.public_key = public_key - self.instance.save() - return self.instance - - -UserPublicKeyForm.verbose_name = _("Public key") - - class UserBulkUpdateForm(OrgModelForm): users = forms.ModelMultipleChoiceField( required=True, @@ -333,40 +188,12 @@ class UserBulkUpdateForm(OrgModelForm): return users -class UserGroupForm(OrgModelForm): - users = forms.ModelMultipleChoiceField( - queryset=User.objects.none(), - label=_("User"), - widget=forms.SelectMultiple( - attrs={ - 'class': 'users-select2', - 'data-placeholder': _('Select users') - } - ), - required=False, +class UserCheckPasswordForm(forms.Form): + password = forms.CharField( + label=_('Password'), widget=forms.PasswordInput, + max_length=128, strip=False ) - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.set_fields_queryset() - def set_fields_queryset(self): - users_field = self.fields.get('users') - if self.instance: - users_field.initial = self.instance.users.all() - users_field.queryset = self.instance.users.all() - else: - users_field.queryset = User.objects.none() - - def save(self, commit=True): - raise Exception("Save by restful api") - - class Meta: - model = UserGroup - fields = [ - 'name', 'users', 'comment', - ] - - -class FileForm(forms.Form): - file = forms.FileField() +class UserCheckOtpCodeForm(forms.Form): + otp_code = forms.CharField(label=_('MFA code'), max_length=6) diff --git a/jumpserver/jumpserver/apps/users/hands.py b/jumpserver/jumpserver/apps/users/hands.py index 0792fa0996855c925b50b64d00283c6e88b6a5d7..5e2007c8aea2f46c6cf5366e81db6145928b7a1c 100644 --- a/jumpserver/jumpserver/apps/users/hands.py +++ b/jumpserver/jumpserver/apps/users/hands.py @@ -11,7 +11,6 @@ """ # from terminal.models import Terminal -# from audits.tasks import write_login_log_async # from users.models import User # from perms.models import AssetPermission # from perms.utils import get_user_granted_assets, get_user_granted_asset_groups diff --git a/jumpserver/jumpserver/apps/users/migrations/0024_auto_20191118_1612.py b/jumpserver/jumpserver/apps/users/migrations/0024_auto_20191118_1612.py new file mode 100644 index 0000000000000000000000000000000000000000..eb368be2f739ea8099288a8a4c9b5b9d749c3888 --- /dev/null +++ b/jumpserver/jumpserver/apps/users/migrations/0024_auto_20191118_1612.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.5 on 2019-11-18 08:12 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0023_auto_20190724_1525'), + ] + + operations = [ + migrations.RenameField( + model_name='user', + old_name='otp_level', + new_name='mfa_level', + ), + ] diff --git a/jumpserver/jumpserver/apps/users/models/group.py b/jumpserver/jumpserver/apps/users/models/group.py index e211759bc3058b93eff7f41f65c43848f28ae0a7..0ae636f7628f745a0d870aeea8d2869429e41d6b 100644 --- a/jumpserver/jumpserver/apps/users/models/group.py +++ b/jumpserver/jumpserver/apps/users/models/group.py @@ -4,6 +4,7 @@ import uuid from django.db import models, IntegrityError from django.utils.translation import ugettext_lazy as _ +from common.utils import lazyproperty from orgs.mixins.models import OrgModelMixin __all__ = ['UserGroup'] @@ -20,6 +21,10 @@ class UserGroup(OrgModelMixin): def __str__(self): return self.name + @lazyproperty + def users_amount(self): + return self.users.count() + class Meta: ordering = ['name'] unique_together = [('org_id', 'name'),] diff --git a/jumpserver/jumpserver/apps/users/models/user.py b/jumpserver/jumpserver/apps/users/models/user.py index aaa3bcebb2e4734f11f5a6fe21d68d91d672cd9f..a062f6c99cba16d0c7504ba2fe58807e7d18a608 100644 --- a/jumpserver/jumpserver/apps/users/models/user.py +++ b/jumpserver/jumpserver/apps/users/models/user.py @@ -11,19 +11,19 @@ from django.contrib.auth.hashers import make_password from django.contrib.auth.models import AbstractUser from django.core.cache import cache from django.db import models + from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from django.shortcuts import reverse from orgs.utils import current_org -from common.utils import get_signer, date_expired_default, get_logger +from common.utils import signer, date_expired_default, get_logger, lazyproperty from common import fields +from ..signals import post_user_change_password __all__ = ['User'] -signer = get_signer() - logger = get_logger(__file__) @@ -42,14 +42,10 @@ class AuthMixin: self.set_password(password_raw_) def set_password(self, raw_password): - self._set_password = True if self.can_update_password(): self.date_password_last_updated = timezone.now() + post_user_change_password.send(self.__class__, user=self) super().set_password(raw_password) - else: - error = _("User auth from {}, go there change password").format( - self.source) - raise PermissionError(error) def can_update_password(self): return self.is_local @@ -60,10 +56,6 @@ class AuthMixin: def can_use_ssh_key_login(self): return settings.TERMINAL_PUBLIC_KEY_AUTH - def check_otp(self, code): - from ..utils import check_otp_code - return check_otp_code(self.otp_secret_key, code) - def is_public_key_valid(self): """ Check if the user's ssh public key is valid. @@ -115,6 +107,30 @@ class AuthMixin: return True return False + def get_login_confirm_setting(self): + if hasattr(self, 'login_confirm_setting'): + s = self.login_confirm_setting + if s.reviewers.all().count() and s.is_active: + return s + return False + + @staticmethod + def get_public_key_body(key): + for i in key.split(): + if len(i) > 256: + return i + return key + + def check_public_key(self, key): + if not self.public_key: + return False + key = self.get_public_key_body(key) + key_saved = self.get_public_key_body(self.public_key) + if key == key_saved: + return True + else: + return False + class RoleMixin: ROLE_ADMIN = 'Admin' @@ -175,53 +191,53 @@ class RoleMixin: def is_app(self): return self.role == 'App' - @property + @lazyproperty def user_orgs(self): from orgs.models import Organization return Organization.get_user_user_orgs(self) - @property + @lazyproperty def admin_orgs(self): from orgs.models import Organization return Organization.get_user_admin_orgs(self) - @property + @lazyproperty def audit_orgs(self): from orgs.models import Organization return Organization.get_user_audit_orgs(self) - @property + @lazyproperty def admin_or_audit_orgs(self): from orgs.models import Organization return Organization.get_user_admin_or_audit_orgs(self) - @property + @lazyproperty def is_org_admin(self): if self.is_superuser or self.related_admin_orgs.exists(): return True else: return False - @property + @lazyproperty def is_org_auditor(self): if self.is_super_auditor or self.related_audit_orgs.exists(): return True else: return False - @property + @lazyproperty def can_admin_current_org(self): return current_org.can_admin_by(self) - @property + @lazyproperty def can_audit_current_org(self): return current_org.can_audit_by(self) - @property + @lazyproperty def can_user_current_org(self): return current_org.can_user_by(self) - @property + @lazyproperty def can_admin_or_audit_current_org(self): return self.can_admin_current_org or self.can_audit_current_org @@ -246,6 +262,16 @@ class RoleMixin: access_key = app.create_access_key() return app, access_key + def remove(self): + if not current_org.is_real(): + return + if self.can_user_current_org: + current_org.users.remove(self) + if self.can_admin_current_org: + current_org.admins.remove(self) + if self.can_audit_current_org: + current_org.auditors.remove(self) + class TokenMixin: CACHE_KEY_USER_RESET_PASSWORD_PREFIX = "_KEY_USER_RESET_PASSWORD_{}" @@ -328,35 +354,70 @@ class TokenMixin: class MFAMixin: - otp_level = 0 + mfa_level = 0 otp_secret_key = '' - OTP_LEVEL_CHOICES = ( + MFA_LEVEL_CHOICES = ( (0, _('Disable')), (1, _('Enable')), (2, _("Force enable")), ) @property - def otp_enabled(self): - return self.otp_force_enabled or self.otp_level > 0 + def mfa_enabled(self): + return self.mfa_force_enabled or self.mfa_level > 0 @property - def otp_force_enabled(self): + def mfa_force_enabled(self): if settings.SECURITY_MFA_AUTH: return True - return self.otp_level == 2 + return self.mfa_level == 2 - def enable_otp(self): - if not self.otp_level == 2: - self.otp_level = 1 + def enable_mfa(self): + if not self.mfa_level == 2: + self.mfa_level = 1 - def force_enable_otp(self): - self.otp_level = 2 + def force_enable_mfa(self): + self.mfa_level = 2 - def disable_otp(self): - self.otp_level = 0 + def disable_mfa(self): + self.mfa_level = 0 self.otp_secret_key = None + def reset_mfa(self): + if self.mfa_is_otp(): + self.otp_secret_key = '' + + @staticmethod + def mfa_is_otp(): + if settings.OTP_IN_RADIUS: + return False + return True + + def check_radius(self, code): + from authentication.backends.radius import RadiusBackend + backend = RadiusBackend() + user = backend.authenticate(None, username=self.username, password=code) + if user: + return True + return False + + def check_otp(self, code): + from ..utils import check_otp_code + return check_otp_code(self.otp_secret_key, code) + + def check_mfa(self, code): + if settings.OTP_IN_RADIUS: + return self.check_radius(code) + else: + return self.check_otp(code) + + def mfa_enabled_but_not_set(self): + if self.mfa_enabled and \ + self.mfa_is_otp() and \ + not self.otp_secret_key: + return True + return False + class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): SOURCE_LOCAL = 'local' @@ -364,7 +425,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): SOURCE_OPENID = 'openid' SOURCE_RADIUS = 'radius' SOURCE_CHOICES = ( - (SOURCE_LOCAL, 'Local'), + (SOURCE_LOCAL, _('Local')), (SOURCE_LDAP, 'LDAP/AD'), (SOURCE_OPENID, 'OpenID'), (SOURCE_RADIUS, 'Radius'), @@ -395,8 +456,8 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): phone = models.CharField( max_length=20, blank=True, null=True, verbose_name=_('Phone') ) - otp_level = models.SmallIntegerField( - default=0, choices=MFAMixin.OTP_LEVEL_CHOICES, verbose_name=_('MFA') + mfa_level = models.SmallIntegerField( + default=0, choices=MFAMixin.MFA_LEVEL_CHOICES, verbose_name=_('MFA') ) otp_secret_key = fields.EncryptCharField(max_length=128, blank=True, null=True) # Todo: Auto generate key, let user download @@ -484,6 +545,9 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): return True return False + def set_avatar(self, f): + self.avatar.save(self.username, f) + def avatar_url(self): admin_default = settings.STATIC_URL + "img/avatar/admin.png" user_default = settings.STATIC_URL + "img/avatar/user.png" diff --git a/jumpserver/jumpserver/apps/users/serializers/__init__.py b/jumpserver/jumpserver/apps/users/serializers/__init__.py index 78a695e51dc0de252b30e088bb6ea4e93b3bb387..41812a5c829c4f807372cf7685758793a87b76c5 100644 --- a/jumpserver/jumpserver/apps/users/serializers/__init__.py +++ b/jumpserver/jumpserver/apps/users/serializers/__init__.py @@ -2,3 +2,4 @@ # from .user import * from .group import * +from .realtion import * diff --git a/jumpserver/jumpserver/apps/users/serializers/group.py b/jumpserver/jumpserver/apps/users/serializers/group.py index d27ddc19abd0de791b800c90a47d5e7ac1f3939a..67b21668caf23fef0fd68bcb7e41c1113d6b53fd 100644 --- a/jumpserver/jumpserver/apps/users/serializers/group.py +++ b/jumpserver/jumpserver/apps/users/serializers/group.py @@ -1,32 +1,32 @@ # -*- coding: utf-8 -*- # from django.utils.translation import ugettext_lazy as _ + from rest_framework import serializers -from common.fields import StringManyToManyField from common.serializers import AdaptedBulkListSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from django.db.models import Count from ..models import User, UserGroup from .. import utils - __all__ = [ - 'UserGroupSerializer', 'UserGroupListSerializer', - 'UserGroupUpdateMemberSerializer' + 'UserGroupSerializer', ] class UserGroupSerializer(BulkOrgResourceModelSerializer): users = serializers.PrimaryKeyRelatedField( - required=False, many=True, queryset=User.objects, label=_('User') + required=False, many=True, queryset=User.objects, label=_('User'), + write_only=True ) class Meta: model = UserGroup list_serializer_class = AdaptedBulkListSerializer fields = [ - 'id', 'name', 'users', 'comment', 'date_created', - 'created_by', + 'id', 'name', 'users', 'users_amount', 'comment', + 'date_created', 'created_by', ] extra_kwargs = { 'created_by': {'label': _('Created by'), 'read_only': True} @@ -47,23 +47,8 @@ class UserGroupSerializer(BulkOrgResourceModelSerializer): raise serializers.ValidationError(msg) return users - -class UserGroupListSerializer(UserGroupSerializer): - users = StringManyToManyField(many=True, read_only=True) - - -class UserGroupUpdateMemberSerializer(serializers.ModelSerializer): - users = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects) - - class Meta: - model = UserGroup - fields = ['id', 'users'] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.set_fields_queryset() - - def set_fields_queryset(self): - users_field = self.fields['users'] - users_field.child_relation.queryset = utils.get_current_org_members() - + @classmethod + def setup_eager_loading(cls, queryset): + """ Perform necessary eager loading of data. """ + queryset = queryset.annotate(users_amount=Count('users')) + return queryset diff --git a/jumpserver/jumpserver/apps/users/serializers/realtion.py b/jumpserver/jumpserver/apps/users/serializers/realtion.py new file mode 100644 index 0000000000000000000000000000000000000000..b768c57fc099df1bb55fc4a5df3f1fe9f34930dc --- /dev/null +++ b/jumpserver/jumpserver/apps/users/serializers/realtion.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# +from rest_framework import serializers + +from ..models import User + +__all__ = ['UserUserGroupRelationSerializer'] + + +class UserUserGroupRelationSerializer(serializers.ModelSerializer): + user_display = serializers.CharField(read_only=True) + usergroup_display = serializers.CharField(read_only=True) + + class Meta: + model = User.groups.through + fields = [ + 'id', 'user', 'user_display', 'usergroup', 'usergroup_display' + ] diff --git a/jumpserver/jumpserver/apps/users/serializers/user.py b/jumpserver/jumpserver/apps/users/serializers/user.py index 57e2f43fa6d2b98533638eeb75cb663f3c2dbf86..da9b725eceba6930ab56f3860ba331fddc7c9cb3 100644 --- a/jumpserver/jumpserver/apps/users/serializers/user.py +++ b/jumpserver/jumpserver/apps/users/serializers/user.py @@ -1,64 +1,44 @@ # -*- coding: utf-8 -*- # from django.utils.translation import ugettext_lazy as _ - from rest_framework import serializers from common.utils import validate_ssh_public_key from common.mixins import BulkSerializerMixin from common.serializers import AdaptedBulkListSerializer from common.permissions import CanUpdateDeleteUser -from ..models import User, UserGroup +from ..models import User __all__ = [ - 'UserSerializer', 'UserPKUpdateSerializer', 'UserUpdateGroupSerializer', + 'UserSerializer', 'UserPKUpdateSerializer', 'ChangeUserPasswordSerializer', 'ResetOTPSerializer', + 'UserProfileSerializer', 'UserDisplaySerializer', ] class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): - can_update = serializers.SerializerMethodField() - can_delete = serializers.SerializerMethodField() - class Meta: model = User list_serializer_class = AdaptedBulkListSerializer fields = [ 'id', 'name', 'username', 'password', 'email', 'public_key', - 'groups', 'groups_display', - 'role', 'role_display', 'wechat', 'phone', 'otp_level', - 'comment', 'source', 'source_display', 'is_valid', 'is_expired', + 'groups', 'role', 'wechat', 'phone', 'mfa_level', + 'comment', 'source', 'is_valid', 'is_expired', 'is_active', 'created_by', 'is_first_login', 'date_password_last_updated', 'date_expired', 'avatar_url', - 'can_update', 'can_delete', ] extra_kwargs = { 'password': {'write_only': True, 'required': False, 'allow_null': True, 'allow_blank': True}, 'public_key': {'write_only': True}, - 'groups_display': {'label': _('Groups name')}, - 'source_display': {'label': _('Source name')}, 'is_first_login': {'label': _('Is first login'), 'read_only': True}, - 'role_display': {'label': _('Role name')}, 'is_valid': {'label': _('Is valid')}, 'is_expired': {'label': _('Is expired')}, 'avatar_url': {'label': _('Avatar url')}, 'created_by': {'read_only': True, 'allow_blank': True}, - 'can_update': {'read_only': True}, - 'can_delete': {'read_only': True}, } - def get_can_update(self, obj): - return CanUpdateDeleteUser.has_update_object_permission( - self.context['request'], self.context['view'], obj - ) - - def get_can_delete(self, obj): - return CanUpdateDeleteUser.has_delete_object_permission( - self.context['request'], self.context['view'], obj - ) - def validate_role(self, value): request = self.context.get('request') if not request.user.is_superuser and value != User.ROLE_USER: @@ -94,11 +74,52 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): attrs['password_raw'] = password return attrs + @staticmethod + def clean_auth_fields(attrs): + for field in ('password', 'public_key'): + value = attrs.get(field) + if not value: + attrs.pop(field, None) + return attrs + def validate(self, attrs): attrs = self.change_password_to_raw(attrs) + attrs = self.clean_auth_fields(attrs) return attrs +class UserDisplaySerializer(UserSerializer): + can_update = serializers.SerializerMethodField() + can_delete = serializers.SerializerMethodField() + + class Meta(UserSerializer.Meta): + fields = UserSerializer.Meta.fields + [ + 'groups_display', 'role_display', 'source_display', + 'can_update', 'can_delete', + ] + + def get_can_update(self, obj): + return CanUpdateDeleteUser.has_update_object_permission( + self.context['request'], self.context['view'], obj + ) + + def get_can_delete(self, obj): + return CanUpdateDeleteUser.has_delete_object_permission( + self.context['request'], self.context['view'], obj + ) + + def get_extra_kwargs(self): + kwargs = super().get_extra_kwargs() + kwargs.update({ + 'can_update': {'read_only': True}, + 'can_delete': {'read_only': True}, + 'groups_display': {'label': _('Groups name')}, + 'source_display': {'label': _('Source name')}, + 'role_display': {'label': _('Role name')}, + }) + return kwargs + + class UserPKUpdateSerializer(serializers.ModelSerializer): class Meta: model = User @@ -111,16 +132,6 @@ class UserPKUpdateSerializer(serializers.ModelSerializer): return value -class UserUpdateGroupSerializer(serializers.ModelSerializer): - groups = serializers.PrimaryKeyRelatedField( - many=True, queryset=UserGroup.objects - ) - - class Meta: - model = User - fields = ['id', 'groups'] - - class ChangeUserPasswordSerializer(serializers.ModelSerializer): class Meta: @@ -130,3 +141,17 @@ class ChangeUserPasswordSerializer(serializers.ModelSerializer): class ResetOTPSerializer(serializers.Serializer): msg = serializers.CharField(read_only=True) + + def create(self, validated_data): + pass + + def update(self, instance, validated_data): + pass + + +class UserProfileSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = [ + 'id', 'username', 'name', 'role', 'email' + ] diff --git a/jumpserver/jumpserver/apps/users/signals.py b/jumpserver/jumpserver/apps/users/signals.py index b9084b7dc64d0b0b465c4bc988a39e33e9146966..37969f839d1457bb8a3c21185b726b6b87a6a459 100644 --- a/jumpserver/jumpserver/apps/users/signals.py +++ b/jumpserver/jumpserver/apps/users/signals.py @@ -2,3 +2,4 @@ from django.dispatch import Signal post_user_create = Signal(providing_args=('user',)) +post_user_change_password = Signal(providing_args=('user',)) diff --git a/jumpserver/jumpserver/apps/users/signals_handler.py b/jumpserver/jumpserver/apps/users/signals_handler.py index a33d9eec9c4c0baafaf0c18e75edd27f12bd84fa..902dfa4d78a4b9a4abd7db8e58401487f1d3dbe8 100644 --- a/jumpserver/jumpserver/apps/users/signals_handler.py +++ b/jumpserver/jumpserver/apps/users/signals_handler.py @@ -2,7 +2,7 @@ # from django.dispatch import receiver -from django.db.models.signals import post_save, m2m_changed +from django.db.models.signals import m2m_changed from common.utils import get_logger from .signals import post_user_create diff --git a/jumpserver/jumpserver/apps/users/templates/users/_base_otp.html b/jumpserver/jumpserver/apps/users/templates/users/_base_otp.html index ac511efb03b1e9aae17e460d81e55c2412e0c002..89ac8d656ef1810e3f1b6df74a4d3e17068fdf73 100644 --- a/jumpserver/jumpserver/apps/users/templates/users/_base_otp.html +++ b/jumpserver/jumpserver/apps/users/templates/users/_base_otp.html @@ -1,60 +1,20 @@ +{% extends '_without_nav_base.html' %} {% load static %} {% load i18n %} - - - - - {{ JMS_TITLE }} - - - - - - - - - - -
    - - -
    - - -
    -
    -

    - {% block small_title %} - {% endblock %} -

    -
    -
    -
    {% trans 'Security token validation' %}  {% trans 'Account' %} {{ user.username }}  {% trans 'Follow these steps to complete the binding operation' %}
    -
    - {% block content %} +{% block body %} +
    +
    +

    + {% block small_title %} {% endblock %} -

    -
    - - -
    -
    - {% include '_copyright.html' %} -
    -
    - - - - + +
    +
    +
    {% trans 'Security token validation' %}  {% trans 'Account' %} {{ user.username }}  {% trans 'Follow these steps to complete the binding operation' %}
    +
    + {% block content %} + {% endblock %} +
    +
    +{% endblock %} diff --git a/jumpserver/jumpserver/apps/users/templates/users/_base_user_detail.html b/jumpserver/jumpserver/apps/users/templates/users/_base_user_detail.html new file mode 100644 index 0000000000000000000000000000000000000000..936037ab8a7c210971c7e03f5546f304ef3e4410 --- /dev/null +++ b/jumpserver/jumpserver/apps/users/templates/users/_base_user_detail.html @@ -0,0 +1,26 @@ +{% extends 'base.html' %} +{% load static %} +{% load i18n %} + +{% block content %} +
    +
    +
    +
    +
    + +
    +
    + {% block content_table %} + {% endblock %} +
    +
    +
    +
    +
    +{% endblock %} diff --git a/jumpserver/jumpserver/apps/users/templates/users/_user.html b/jumpserver/jumpserver/apps/users/templates/users/_user.html index ea0f76854d1cfc594def9f997eeefac765b9e49a..dbcf7f804f79f360a5ec3314beed8b872ba41f5c 100644 --- a/jumpserver/jumpserver/apps/users/templates/users/_user.html +++ b/jumpserver/jumpserver/apps/users/templates/users/_user.html @@ -20,7 +20,7 @@

    {% trans 'Auth' %}

    {% block password %}{% endblock %} - {% bootstrap_field form.otp_level layout="horizontal" %} + {% bootstrap_field form.mfa_level layout="horizontal" %} {% bootstrap_field form.source layout="horizontal" %}
    @@ -57,6 +57,7 @@ {% endblock %} {% block custom_foot_js %} + @@ -73,20 +74,10 @@ $(groups_id).closest('.form-group').removeClass('hidden'); }} - var dateOptions = { - singleDatePicker: true, - showDropdowns: true, - timePicker: true, - timePicker24Hour: true, - autoApply: true, - locale: { - format: 'YYYY-MM-DD HH:mm' - } - }; $(document).ready(function () { $('.select2').select2(); - $('#id_date_expired').daterangepicker(dateOptions); - var mfa_radio = $('#id_otp_level'); + initDateRangePicker('#id_date_expired'); + var mfa_radio = $('#id_mfa_level'); mfa_radio.addClass("form-inline"); mfa_radio.children().css("margin-right","15px"); fieldDisplay() diff --git a/jumpserver/jumpserver/apps/users/templates/users/_user_detail_nav_header.html b/jumpserver/jumpserver/apps/users/templates/users/_user_detail_nav_header.html new file mode 100644 index 0000000000000000000000000000000000000000..28079e149e09a1e7405e27edd6cdb01488c7e296 --- /dev/null +++ b/jumpserver/jumpserver/apps/users/templates/users/_user_detail_nav_header.html @@ -0,0 +1,97 @@ +{% load static %} +{% load i18n %} + + + +
  • + {% trans 'User detail' %} +
  • +
  • + + {% trans "User permissions" %} + + + + +
  • + + \ No newline at end of file diff --git a/jumpserver/jumpserver/apps/users/templates/users/_user_groups_import_modal.html b/jumpserver/jumpserver/apps/users/templates/users/_user_groups_import_modal.html deleted file mode 100644 index 63d0572153299cbc9be20df38158cb3d9b576ed4..0000000000000000000000000000000000000000 --- a/jumpserver/jumpserver/apps/users/templates/users/_user_groups_import_modal.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends '_import_modal.html' %} -{% load i18n %} - -{% block modal_title%}{% trans "Import user groups" %}{% endblock %} - -{% block import_modal_download_template_url %}{% url "api-users:user-group-list" %}{% endblock %} \ No newline at end of file diff --git a/jumpserver/jumpserver/apps/users/templates/users/_user_groups_update_modal.html b/jumpserver/jumpserver/apps/users/templates/users/_user_groups_update_modal.html deleted file mode 100644 index a07c0f82c21d5dee530f70ed288aeaecff0c4dd4..0000000000000000000000000000000000000000 --- a/jumpserver/jumpserver/apps/users/templates/users/_user_groups_update_modal.html +++ /dev/null @@ -1,4 +0,0 @@ -{% extends '_update_modal.html' %} -{% load i18n %} - -{% block modal_title%}{% trans "Update user group" %}{% endblock %} \ No newline at end of file diff --git a/jumpserver/jumpserver/apps/users/templates/users/_user_import_modal.html b/jumpserver/jumpserver/apps/users/templates/users/_user_import_modal.html deleted file mode 100644 index e53d67fa77ed3eb63c7d23a1a8832702db911691..0000000000000000000000000000000000000000 --- a/jumpserver/jumpserver/apps/users/templates/users/_user_import_modal.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends '_import_modal.html' %} -{% load i18n %} - -{% block modal_title%}{% trans "Import users" %}{% endblock %} - -{% block import_modal_download_template_url %}{% url "api-users:user-list" %}{% endblock %} diff --git a/jumpserver/jumpserver/apps/users/templates/users/_user_update_modal.html b/jumpserver/jumpserver/apps/users/templates/users/_user_update_modal.html deleted file mode 100644 index 9dfe60c96705344c6fd3954216c4f15dfb9a3bdd..0000000000000000000000000000000000000000 --- a/jumpserver/jumpserver/apps/users/templates/users/_user_update_modal.html +++ /dev/null @@ -1,4 +0,0 @@ -{% extends '_update_modal.html' %} -{% load i18n %} - -{% block modal_title%}{% trans "Update user" %}{% endblock %} \ No newline at end of file diff --git a/jumpserver/jumpserver/apps/users/templates/users/first_login.html b/jumpserver/jumpserver/apps/users/templates/users/first_login.html index 1038fb8c8669eff556a55616158dde08d4824e08..fb8af6257bb58609bbcb5d3d3c7fb0f5ab16128e 100644 --- a/jumpserver/jumpserver/apps/users/templates/users/first_login.html +++ b/jumpserver/jumpserver/apps/users/templates/users/first_login.html @@ -13,7 +13,7 @@ {% block content %}
    -
    +
    {% trans 'First Login' %}
    @@ -55,7 +55,7 @@
    -
    + {% csrf_token %} {{ wizard.management_form }} {#{% if wizard.form.forms %}#} @@ -88,7 +88,7 @@ {% endif %}
    -
    +
    diff --git a/jumpserver/jumpserver/apps/users/templates/users/first_login_done.html b/jumpserver/jumpserver/apps/users/templates/users/first_login_done.html index 5639d2c5de90c17034f2a506f6bd75d6744f4ec6..ae43d032be78702187b4d3b4fbcfd4d23db93fac 100644 --- a/jumpserver/jumpserver/apps/users/templates/users/first_login_done.html +++ b/jumpserver/jumpserver/apps/users/templates/users/first_login_done.html @@ -13,7 +13,7 @@ {% block content %}
    -
    +
    {% trans 'First Login' %}
    diff --git a/jumpserver/jumpserver/apps/users/templates/users/forgot_password.html b/jumpserver/jumpserver/apps/users/templates/users/forgot_password.html index 3f3c7fabb3468c935758f15c2513d7e6942ce9f5..d48cf02779d26f7bba46429025c768a36996c7e0 100644 --- a/jumpserver/jumpserver/apps/users/templates/users/forgot_password.html +++ b/jumpserver/jumpserver/apps/users/templates/users/forgot_password.html @@ -1,60 +1,34 @@ +{% extends '_base_only_content.html' %} {% load static %} {% load i18n %} - - - - - - - - {% trans 'Forgot password' %} - - {% include '_head_css_js.html' %} - - - - - - -
    -
    - -
    -
    - -

    {% trans 'Forgot password' %} ?

    -

    - {% if errors %} -

    {{ errors }}

    - {% endif %} -

    - {% trans 'Input your email, that will send a mail to your' %} -

    - -
    -
    -
    - {% csrf_token %} -
    - -
    - - - -
    -
    -
    -
    -
    -
    -
    -
    -
    - {% include '_copyright.html' %} -
    +{% load bootstrap3 %} +{% block custom_head_css_js %} + +{% endblock %} +{% block html_title %}{% trans 'Forgot password' %}{% endblock %} +{% block title %} {% trans 'Forgot password' %}?{% endblock %} + +{% block content %} + {% if errors %} +

    {{ errors }}

    + {% endif %} +

    + {% trans 'Input your email, that will send a mail to your' %} +

    + +
    +
    +
    + {% csrf_token %} + {% bootstrap_field form.email layout="horizontal" %} + {% bootstrap_field form.captcha layout="horizontal" %} + +
    +{% endblock %} - - - diff --git a/jumpserver/jumpserver/apps/users/templates/users/reset_password.html b/jumpserver/jumpserver/apps/users/templates/users/reset_password.html index 6817f0a75b67cd2edd9d7b232cd1a0056fad2323..ecad676bf81608bd9e6baff046fabc3e7b251006 100644 --- a/jumpserver/jumpserver/apps/users/templates/users/reset_password.html +++ b/jumpserver/jumpserver/apps/users/templates/users/reset_password.html @@ -1,136 +1,80 @@ +{% extends '_base_only_content.html' %} {% load static %} {% load i18n %} - - - - - - - {{ JMS_TITLE }} - - {% include '_head_css_js.html' %} - - - - - - - - - -
    -
    - -
    -

    {% trans 'Welcome to the Jumpserver open source fortress' %}

    - -

    - {% trans 'Jumpserver is an open source desktop system developed using Python and Django that helps Internet businesses with efficient users, assets, permissions, and audit management' %} -

    - -

    - {% trans 'We are from all over the world, we have great admiration and worship for the spirit of open source, we have endless pursuit for perfection, neatness and elegance' %} -

    - -

    - {% trans 'We focus on automatic operation and maintenance, and strive to build an easy-to-use, stable, safe and automatic board hopping machine, which is our unremitting pursuit and power' %} -

    - -

    - {% trans 'Always young, always with tears in my eyes. Stay foolish Stay hungry' %} -

    - -
    -
    -
    -
    {% trans 'Reset password' %}
    -
    - {% csrf_token %} - {% if errors %} -

    {{ errors }}

    - {% endif %} -
    - - {# 密码popover #} -
    - -
    -
    -
    - -
    - - - - Forgot password? - - -

    -

    -
    -

    -

    +{% load bootstrap3 %} +{% block html_title %}{% trans 'Reset password' %}{% endblock %} +{% block title %}{% trans 'Reset password' %}{% endblock %} + +{% block content %} +
    + {% csrf_token %} + {% if errors %} +

    {{ errors }}

    + {% endif %} + {% if not token_invalid %} +
    + {% bootstrap_field form.new_password %} + {% bootstrap_field form.confirm_password %} + {# 密码popover #} +
    +
    -
    -
    -
    - {% include '_copyright.html' %} -
    -
    -
    - - - - - + +{% endblock %} diff --git a/jumpserver/jumpserver/apps/users/templates/users/user_asset_permission.html b/jumpserver/jumpserver/apps/users/templates/users/user_asset_permission.html new file mode 100644 index 0000000000000000000000000000000000000000..21ae72722470c6ed20b249b9c536c0b98f935102 --- /dev/null +++ b/jumpserver/jumpserver/apps/users/templates/users/user_asset_permission.html @@ -0,0 +1,201 @@ +{% extends 'users/_base_user_detail.html' %} +{% load static %} +{% load i18n %} + +{% block custom_head_css_js %} + + +{% endblock %} + + +{% block content_table %} +
    +
    +
    + {{ object.name }} +
    + + + + + + + + + + +
    +
    +
    + + + + + + + + + + + + + + + + +
    {% trans 'Name' %}{% trans 'User' %}{% trans 'User group' %}{% trans 'Asset' %}{% trans 'Node' %}{% trans 'System user' %}{% trans 'Validity' %}{% trans 'Action' %}
    +
    +
    +
    + +{% include '_filter_dropdown.html' %} + +{% endblock %} + +{% block custom_foot_js %} + +{% endblock %} diff --git a/jumpserver/jumpserver/apps/users/templates/users/user_database_app_permission.html b/jumpserver/jumpserver/apps/users/templates/users/user_database_app_permission.html new file mode 100644 index 0000000000000000000000000000000000000000..73b3a7972c493eb5a1f7fcd23f48bd3d686026fe --- /dev/null +++ b/jumpserver/jumpserver/apps/users/templates/users/user_database_app_permission.html @@ -0,0 +1,168 @@ +{% extends 'users/_base_user_detail.html' %} +{% load static %} +{% load i18n %} + +{% block custom_head_css_js %} + + +{% endblock %} + +{% block content_table %} +
    +
    +
    + {{ object.name }} +
    + + + + + + + + + + +
    +
    +
    + + + + + + + + + + + + + + + +
    {% trans 'Name' %}{% trans 'User' %}{% trans 'User group' %}{% trans 'DatabaseApp' %}{% trans 'System user' %}{% trans 'Validity' %}{% trans 'Action' %}
    +
    +
    +
    +{% endblock %} + +{% block custom_foot_js %} + +{% endblock %} diff --git a/jumpserver/jumpserver/apps/users/templates/users/user_detail.html b/jumpserver/jumpserver/apps/users/templates/users/user_detail.html index 2c4fad1c9a70e5f775c9687b8d0805eb28e51168..7bac7a4540c655e8b0335f87c96338e25b7c1cc5 100644 --- a/jumpserver/jumpserver/apps/users/templates/users/user_detail.html +++ b/jumpserver/jumpserver/apps/users/templates/users/user_detail.html @@ -1,312 +1,354 @@ -{% extends 'base.html' %} +{% extends 'users/_base_user_detail.html' %} {% load static %} {% load i18n %} {% block custom_head_css_js %} - - {% endblock %} -{% block content %} -
    -
    -
    -
    - -
    -
    -
    -
    - {{ user_object.name }} -
    - - - - - - - - - - +{% block content_nav_delete_update %} +
  • + {% trans 'Update' %} +
  • +
  • + + {% trans 'Delete' %} + +
  • +{% endblock %} + +{% block content_table %} +
    +
    +
    + {{ object.name }} +
    + + + + + + + + + + +
    +
    +
    + + + + + + + + + + + + + + + + + + {% if user.phone %} + + + + + {% endif %} + {% if object.wechat %} + + + + + {% endif %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if object.can_update_password %} + + + + + {% endif %} + + + + + +
    + +
    {% trans 'Name' %}:{{ object.name }}
    {% trans 'Username' %}:{{ object.username }}
    {% trans 'Email' %}:{{ object.email }}
    {% trans 'Phone' %}:{{ object.phone }}
    {% trans 'Wechat' %}:{{ object.wechat }}
    {% trans 'Role' %}:{{ object.role_display }}
    {% trans 'MFA certification' %}: + {% if object.mfa_force_enabled %} + {% trans 'Force enabled' %} + {% elif object.mfa_enabled%} + {% trans 'Enabled' %} + {% else %} + {% trans 'Disabled' %} + {% endif %} +
    {% trans 'Source' %}:{{ object.get_source_display }}
    {% trans 'Date expired' %}:{{ object.date_expired|date:"Y-m-j H:i:s" }}
    {% trans 'Created by' %}:{{ object.created_by }}
    {% trans 'Date joined' %}:{{ object.date_joined|date:"Y-m-j H:i:s" }}
    {% trans 'Last login' %}:{{ object.last_login|date:"Y-m-j H:i:s" }}
    {% trans 'Last password updated' %}:{{ object.date_password_last_updated|date:"Y-m-j H:i:s" }}
    {% trans 'Comment' %}:{{ object.comment }}
    +
    +
    +
    +
    +
    +
    + {% trans 'Quick modify' %} +
    +
    + + + + + + + + + - - - - {% endfor %} - -
    {% trans 'Active' %}: + +
    +
    + +
    -
    - - - - - - - - - - - - - - - - - - {% if user.phone %} - - - - - {% endif %} - {% if user_object.wechat %} - - - - - {% endif %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {% if user_object.can_update_password %} - - - - - {% endif %} - - - - - -
    - -
    {% trans 'Name' %}:{{ user_object.name }}
    {% trans 'Username' %}:{{ user_object.username }}
    {% trans 'Email' %}:{{ user_object.email }}
    {% trans 'Phone' %}:{{ user_object.phone }}
    {% trans 'Wechat' %}:{{ user_object.wechat }}
    {% trans 'Role' %}:{{ user_object.role_display }}
    {% trans 'MFA certification' %}: - {% if user_object.otp_force_enabled %} - {% trans 'Force enabled' %} - {% elif user_object.otp_enabled%} - {% trans 'Enabled' %} - {% else %} - {% trans 'Disabled' %} - {% endif %} -
    {% trans 'Source' %}:{{ user_object.get_source_display }}
    {% trans 'Date expired' %}:{{ user_object.date_expired|date:"Y-m-j H:i:s" }}
    {% trans 'Created by' %}:{{ user_object.created_by }}
    {% trans 'Date joined' %}:{{ user_object.date_joined|date:"Y-m-j H:i:s" }}
    {% trans 'Last login' %}:{{ user_object.last_login|date:"Y-m-j H:i:s" }}
    {% trans 'Last password updated' %}:{{ user_object.date_password_last_updated|date:"Y-m-j H:i:s" }}
    {% trans 'Comment' %}:{{ user_object.comment }}
    + +
    {% trans 'Force enabled MFA' %}: + +
    +
    + +
    - -
    -
    -
    - {% trans 'Quick modify' %} -
    -
    - - - - - - - - - - - - - - - {% if user_object.can_update_password %} - - - - - {% endif %} - {% if user_object.can_update_ssh_key %} - - - - - {% endif %} - - - - - -
    {% trans 'Active' %}: -
    -
    - - -
    -
    -
    {% trans 'Force enabled MFA' %}: -
    -
    - - -
    -
    -
    {% trans 'Reset MFA' %}: - - - -
    {% trans 'Send reset password mail' %}: - - - -
    {% trans 'Send reset ssh key mail' %}: - - - -
    {% trans 'Unblock user' %} - - - -
    -
    -
    - {% if request.user.can_admin_current_org %} - {% if user_object.can_user_current_org or user_object.can_admin_current_org %} -
    -
    - {% trans 'User group' %} -
    -
    - - - - - - - - - - + + + + + + + + {% if object.can_update_password %} + + + + + {% endif %} + {% if object.can_update_ssh_key %} + + + + + {% endif %} + + + + + +
    - -
    - -
    {% trans 'Reset MFA' %}: + + + +
    {% trans 'Send reset password mail' %}: + + + +
    {% trans 'Send reset ssh key mail' %}: + + + +
    {% trans 'Unblock user' %} + + + +
    +
    +
    + {% if request.user.can_admin_current_org %} - {% for group in user_object.groups.all %} -
    - {{ group.name }} - - -
    -
    -
    - {% endif %} + {% if object.can_user_current_org or object.can_admin_current_org %} +
    +
    + {% trans 'User group' %} +
    +
    + + + + + + + + + + + + {% for group in object.groups.all %} + + + + + {% endfor %} + +
    + +
    + +
    + {{ group.name }} + + +
    +
    +
    + {% endif %} + + {% if LICENSE_VALID and LOGIN_CONFIRM_ENABLE %} +
    +
    + {% trans 'Login confirm' %} +
    +
    + + + + + + + + + + + {% if object.get_login_confirm_setting %} + {% for u in object.login_confirm_setting.reviewers.all %} + + + + + {% endfor %} {% endif %} - + +
    + +
    + +
    + {{ u }} + + +
    -
    -
    + {% endif %} + + {% endif %}
    - {% include 'users/_user_update_pk_modal.html' %} + +{% include 'users/_user_update_pk_modal.html' %} + {% endblock %} + {% block custom_foot_js %} {% endblock %} diff --git a/jumpserver/jumpserver/apps/users/templates/users/user_otp_authentication.html b/jumpserver/jumpserver/apps/users/templates/users/user_disable_mfa.html similarity index 100% rename from jumpserver/jumpserver/apps/users/templates/users/user_otp_authentication.html rename to jumpserver/jumpserver/apps/users/templates/users/user_disable_mfa.html diff --git a/jumpserver/jumpserver/apps/users/templates/users/user_granted_asset.html b/jumpserver/jumpserver/apps/users/templates/users/user_granted_asset.html index 184537cd4756f61cc18fa7020b8ed6ecd098251c..5236971538aa836e4d9068c339ed001310341e04 100644 --- a/jumpserver/jumpserver/apps/users/templates/users/user_granted_asset.html +++ b/jumpserver/jumpserver/apps/users/templates/users/user_granted_asset.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends 'users/_base_user_detail.html' %} {% load bootstrap3 %} {% load static %} {% load i18n %} @@ -7,29 +7,11 @@ {% endblock %} -{% block content %} -
    -
    -
    -
    - -
    - {% include 'users/_granted_assets.html' %} -
    -
    -
    -
    -
    + +{% block content_table %} +{% include 'users/_granted_assets.html' %} {% endblock %} + {% block custom_foot_js %} +{% endblock %} + +{% block content_table %} +
    +
    +
    + {{ object.name }} +
    + + + + + + + + + + +
    +
    +
    + + + + + + + + + + + + + +
    + + {% trans 'Name' %}{% trans 'Type' %}{% trans 'Host' %}{% trans 'Database' %}{% trans 'Comment' %}
    +
    +
    +
    +{% endblock %} + +{% block custom_foot_js %} + +{% endblock %} diff --git a/jumpserver/jumpserver/apps/users/templates/users/user_granted_remote_app.html b/jumpserver/jumpserver/apps/users/templates/users/user_granted_remote_app.html new file mode 100644 index 0000000000000000000000000000000000000000..19b8ab22cca5affbe26a10171dee20a17507fd34 --- /dev/null +++ b/jumpserver/jumpserver/apps/users/templates/users/user_granted_remote_app.html @@ -0,0 +1,93 @@ +{% extends 'users/_base_user_detail.html' %} +{% load i18n static %} + +{% block custom_head_css_js %} + +{% endblock %} + +{% block content_table %} +
    +
    +
    + {{ object.name }} +
    + + + + + + + + + + +
    +
    +
    + + + + + + + + + + + + +
    + + {% trans 'Name' %}{% trans 'App type' %}{% trans 'Asset' %}{% trans 'Comment' %}
    +
    +
    +
    +{% endblock %} + +{% block custom_foot_js %} + +{% endblock %} diff --git a/jumpserver/jumpserver/apps/users/templates/users/user_group_create_update.html b/jumpserver/jumpserver/apps/users/templates/users/user_group_create_update.html index 25f027ad542a549a58900ba97f437a6bee4c70be..09030eca87f8f0ed0e379cb6c73e6119d44a227a 100644 --- a/jumpserver/jumpserver/apps/users/templates/users/user_group_create_update.html +++ b/jumpserver/jumpserver/apps/users/templates/users/user_group_create_update.html @@ -2,10 +2,6 @@ {% load static %} {% load i18n %} {% load bootstrap3 %} -{% block custom_head_css_js %} - - -{% endblock %} {% block content %}
    diff --git a/jumpserver/jumpserver/apps/users/templates/users/user_group_detail.html b/jumpserver/jumpserver/apps/users/templates/users/user_group_detail.html index 8f5cccf29211b686c8532722e27a9e4c1286d3ef..cd65bb746a3338cfca131084b662052494fccd30 100644 --- a/jumpserver/jumpserver/apps/users/templates/users/user_group_detail.html +++ b/jumpserver/jumpserver/apps/users/templates/users/user_group_detail.html @@ -3,13 +3,8 @@ {% load i18n %} {% block custom_head_css_js %} - - - - - {% endblock %} {% block content %}
    @@ -96,9 +91,9 @@ {% for user in user_group.users.all %} - {{ user.name }} + {{ user.name }} - + {% endfor %} @@ -115,73 +110,51 @@ {% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/jumpserver/jumpserver/apps/users/templates/users/user_group_list.html b/jumpserver/jumpserver/apps/users/templates/users/user_group_list.html index 3eb70e3195aaceb5c92323b920f82862cc695f6f..5e354d3db85b786fcf43a663fafbb3cc6b055225 100644 --- a/jumpserver/jumpserver/apps/users/templates/users/user_group_list.html +++ b/jumpserver/jumpserver/apps/users/templates/users/user_group_list.html @@ -1,28 +1,7 @@ {% extends '_base_list.html' %} {% load i18n static %} {% block table_search %} - + {% include '_csv_import_export.html' %} {% endblock %} {% block table_container %} @@ -39,14 +18,13 @@ -{% include "users/_user_groups_import_modal.html" %} -{% include "users/_user_groups_update_modal.html" %} {% endblock %} {% block content_bottom_left %}{% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/jumpserver/jumpserver/apps/users/templates/users/user_list.html b/jumpserver/jumpserver/apps/users/templates/users/user_list.html index cd9273681710d245eea467a20368145197c056d1..63dd981befb11539716e6ad722379b8906d6e9ff 100644 --- a/jumpserver/jumpserver/apps/users/templates/users/user_list.html +++ b/jumpserver/jumpserver/apps/users/templates/users/user_list.html @@ -1,32 +1,11 @@ {% extends '_base_list.html' %} {% load i18n static %} {% block table_search %} - + {% include '_csv_import_export.html' %} {% endblock %} {% block table_container %} - +
    @@ -47,7 +26,11 @@
    + + + + + + + + + + + + + + +
    {% trans 'Name' %}{% trans 'User' %}{% trans 'User group' %}{% trans 'RemoteApp' %}{% trans 'System user' %}{% trans 'Validity' %}{% trans 'Action' %}
    +
    +
    + +{% endblock %} + +{% block custom_foot_js %} + +{% endblock %} diff --git a/jumpserver/jumpserver/apps/users/urls/api_urls.py b/jumpserver/jumpserver/apps/users/urls/api_urls.py index 27d5fd5fa11b6c6d7cfcf9affe11b019c0467f25..b3a75f9ba4f86aa6a0982c6196bec7e14c0885b1 100644 --- a/jumpserver/jumpserver/apps/users/urls/api_urls.py +++ b/jumpserver/jumpserver/apps/users/urls/api_urls.py @@ -14,14 +14,12 @@ app_name = 'users' router = BulkRouter() router.register(r'users', api.UserViewSet, 'user') router.register(r'groups', api.UserGroupViewSet, 'user-group') +router.register(r'users-groups-relations', api.UserUserGroupRelationViewSet, 'users-groups-relation') urlpatterns = [ path('connection-token/', auth_api.UserConnectionTokenApi.as_view(), name='connection-token'), - path('auth/', auth_api.UserAuthApi.as_view(), name='user-auth'), - path('otp/auth/', auth_api.UserOtpAuthApi.as_view(), name='user-otp-auth'), - path('profile/', api.UserProfileApi.as_view(), name='user-profile'), path('otp/reset/', api.UserResetOTPApi.as_view(), name='my-otp-reset'), path('users//otp/reset/', api.UserResetOTPApi.as_view(), name='user-reset-otp'), @@ -30,8 +28,6 @@ urlpatterns = [ path('users//pubkey/reset/', api.UserResetPKApi.as_view(), name='user-public-key-reset'), path('users//pubkey/update/', api.UserUpdatePKApi.as_view(), name='user-public-key-update'), path('users//unblock/', api.UserUnblockPKApi.as_view(), name='user-unblock'), - path('users//groups/', api.UserUpdateGroupApi.as_view(), name='user-update-group'), - path('groups//users/', api.UserGroupUpdateUserApi.as_view(), name='user-group-update-user'), ] urlpatterns += router.urls diff --git a/jumpserver/jumpserver/apps/users/urls/views_urls.py b/jumpserver/jumpserver/apps/users/urls/views_urls.py index dbed09888d63b85316ecdd5c16fd146507efbc78..7773ca7d6ebf50d10b0f0075231125238ef95644 100644 --- a/jumpserver/jumpserver/apps/users/urls/views_urls.py +++ b/jumpserver/jumpserver/apps/users/urls/views_urls.py @@ -20,10 +20,10 @@ urlpatterns = [ path('profile/password/update/', views.UserPasswordUpdateView.as_view(), name='user-password-update'), path('profile/pubkey/update/', views.UserPublicKeyUpdateView.as_view(), name='user-pubkey-update'), path('profile/pubkey/generate/', views.UserPublicKeyGenerateView.as_view(), name='user-pubkey-generate'), - path('profile/otp/enable/authentication/', views.UserOtpEnableAuthenticationView.as_view(), name='user-otp-enable-authentication'), + path('profile/otp/enable/authentication/', views.UserCheckPasswordView.as_view(), name='user-otp-enable-authentication'), path('profile/otp/enable/install-app/', views.UserOtpEnableInstallAppView.as_view(), name='user-otp-enable-install-app'), path('profile/otp/enable/bind/', views.UserOtpEnableBindView.as_view(), name='user-otp-enable-bind'), - path('profile/otp/disable/authentication/', views.UserOtpDisableAuthenticationView.as_view(), name='user-otp-disable-authentication'), + path('profile/otp/disable/authentication/', views.UserDisableMFAView.as_view(), name='user-otp-disable-authentication'), path('profile/otp/update/', views.UserOtpUpdateView.as_view(), name='user-otp-update'), path('profile/otp/settings-success/', views.UserOtpSettingsSuccessView.as_view(), name='user-otp-settings-success'), @@ -35,6 +35,11 @@ urlpatterns = [ path('user/update/', views.UserBulkUpdateView.as_view(), name='user-bulk-update'), path('user//', views.UserDetailView.as_view(), name='user-detail'), path('user//assets/', views.UserGrantedAssetView.as_view(), name='user-granted-asset'), + path('user//asset-permissions/', views.UserAssetPermissionListView.as_view(), name='user-asset-permission'), + path('user//remote-apps/', views.UserGrantedRemoteAppView.as_view(), name='user-granted-remote-app'), + path('user//remote-app-permissions/', views.UserRemoteAppPermissionListView.as_view(), name='user-remote-app-permission'), + path('user//database-apps/', views.UserGrantedDatabasesAppView.as_view(), name='user-granted-database-app'), + path('user//database-app-permissions/', views.UserDatabaseAppPermissionListView.as_view(), name='user-database-app-permission'), path('user//login-history/', views.UserDetailView.as_view(), name='user-login-history'), # User group view diff --git a/jumpserver/jumpserver/apps/users/utils.py b/jumpserver/jumpserver/apps/users/utils.py index 30868358c46395c19bc1fb22baa7a0d254afea79..5acb4df9af026a975d6787aec33e83dcf42a1fec 100644 --- a/jumpserver/jumpserver/apps/users/utils.py +++ b/jumpserver/jumpserver/apps/users/utils.py @@ -193,7 +193,6 @@ def send_reset_ssh_key_mail(user): send_mail_async.delay(subject, message, recipient_list, html_message=message) - def get_user_or_tmp_user(request): user = request.user tmp_user = get_tmp_user_from_cache(request) @@ -212,16 +211,24 @@ def get_tmp_user_from_cache(request): return user -def set_tmp_user_to_cache(request, user): - cache.set(request.session.session_key+'user', user, 600) +def set_tmp_user_to_cache(request, user, ttl=3600): + cache.set(request.session.session_key+'user', user, ttl) + + +def delete_tmp_user_for_cache(request): + if not request.session.session_key: + return None + cache.delete(request.session.session_key+'user') def redirect_user_first_login_or_index(request, redirect_field_name): if request.user.is_first_login: return reverse('users:user-first-login') - return request.POST.get( - redirect_field_name, - request.GET.get(redirect_field_name, reverse('index'))) + url_in_post = request.POST.get(redirect_field_name) + if url_in_post: + return url_in_post + url_in_get = request.GET.get(redirect_field_name, reverse('index')) + return url_in_get def generate_otp_uri(request, issuer="Jumpserver"): @@ -328,3 +335,18 @@ def construct_user_email(username, email): def get_current_org_members(exclude=()): from orgs.utils import current_org return current_org.get_org_members(exclude=exclude) + + +def get_source_choices(): + from .models import User + choices_all = dict(User.SOURCE_CHOICES) + choices = [ + (User.SOURCE_LOCAL, choices_all[User.SOURCE_LOCAL]), + ] + if settings.AUTH_LDAP: + choices.append((User.SOURCE_LDAP, choices_all[User.SOURCE_LDAP])) + if settings.AUTH_OPENID: + choices.append((User.SOURCE_OPENID, choices_all[User.SOURCE_OPENID])) + if settings.AUTH_RADIUS: + choices.append((User.SOURCE_RADIUS, choices_all[User.SOURCE_RADIUS])) + return choices diff --git a/jumpserver/jumpserver/apps/users/views/__init__.py b/jumpserver/jumpserver/apps/users/views/__init__.py index b178ac7106d883d6446c6b1fef8bfeadd57bff16..17d4f4110be07ec24007339ce7569caa93fe8348 100644 --- a/jumpserver/jumpserver/apps/users/views/__init__.py +++ b/jumpserver/jumpserver/apps/users/views/__init__.py @@ -2,4 +2,5 @@ from .login import * from .user import * +from .profile import * from .group import * diff --git a/jumpserver/jumpserver/apps/users/views/login.py b/jumpserver/jumpserver/apps/users/views/login.py index 3f0f88e8f512a04d2c04d2dbbfd7e2aa88a8c169..c96c73e9da3023225af4e5650ee265a8d09694d2 100644 --- a/jumpserver/jumpserver/apps/users/views/login.py +++ b/jumpserver/jumpserver/apps/users/views/login.py @@ -4,13 +4,13 @@ from __future__ import unicode_literals from django.shortcuts import render from django.views.generic import RedirectView from django.core.files.storage import default_storage -from django.http import HttpResponseRedirect from django.shortcuts import reverse, redirect from django.utils.translation import ugettext as _ from django.views.generic.base import TemplateView from django.conf import settings from django.urls import reverse_lazy from formtools.wizard.views import SessionWizardView +from django.views.generic import FormView from common.utils import get_object_or_none from common.permissions import PermissionsMixin, IsValidUser @@ -33,22 +33,24 @@ class UserLoginView(RedirectView): query_string = True -class UserForgotPasswordView(TemplateView): +class UserForgotPasswordView(FormView): template_name = 'users/forgot_password.html' + form_class = forms.UserForgotPasswordForm - def post(self, request): - email = request.POST.get('email') + def form_valid(self, form): + request = self.request + email = form.cleaned_data['email'] user = get_object_or_none(User, email=email) if not user: error = _('Email address invalid, please input again') return self.get(request, errors=error) elif not user.can_update_password(): - error = _('User auth from {}, go there change password'.format(user.source)) + error = _('User auth from {}, go there change password'.format( + user.source)) return self.get(request, errors=error) else: send_reset_password_mail(user) - return HttpResponseRedirect( - reverse('users:forgot-password-sendmail-success')) + return redirect('users:forgot-password-sendmail-success') class UserForgotPasswordSendmailSuccessView(TemplateView): @@ -79,44 +81,47 @@ class UserResetPasswordSuccessView(TemplateView): return super().get_context_data(**kwargs) -class UserResetPasswordView(TemplateView): +class UserResetPasswordView(FormView): template_name = 'users/reset_password.html' + form_class = forms.UserTokenResetPasswordForm def get(self, request, *args, **kwargs): - token = request.GET.get('token', '') + context = self.get_context_data(**kwargs) + errors = kwargs.get('errors') + if errors: + context['errors'] = errors + return self.render_to_response(context) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + token = self.request.GET.get('token', '') user = User.validate_reset_password_token(token) if not user: - kwargs.update({'errors': _('Token invalid or expired')}) - else: - check_rules = get_password_check_rules() - kwargs.update({'password_check_rules': check_rules}) - return super().get(request, *args, **kwargs) - - def post(self, request, *args, **kwargs): - password = request.POST.get('password') - password_confirm = request.POST.get('password-confirm') - token = request.GET.get('token') - - if password != password_confirm: - return self.get(request, errors=_('Password not same')) + context['errors'] = _('Token invalid or expired') + context['token_invalid'] = True + check_rules = get_password_check_rules() + context['password_check_rules'] = check_rules + return context + def form_valid(self, form): + token = self.request.GET.get('token') user = User.validate_reset_password_token(token) if not user: - return self.get(request, errors=_('Token invalid or expired')) + return self.get(self.request, errors=_('Token invalid or expired')) + if not user.can_update_password(): - error = _('User auth from {}, go there change password'.format(user.source)) - return self.get(request, errors=error) + errors = _('User auth from {}, go there change password'.format(user.source)) + return self.get(self.request, errors=errors) + password = form.cleaned_data['new_password'] is_ok = check_password_rules(password) if not is_ok: - return self.get( - request, - errors=_('* Your password does not meet the requirements') - ) + errors = _('* Your password does not meet the requirements') + return self.get(self.request, errors=errors) user.reset_password(password) User.expired_reset_password_token(token) - return HttpResponseRedirect(reverse('users:reset-password-success')) + return redirect('users:reset-password-success') class UserFirstLoginView(PermissionsMixin, SessionWizardView): @@ -170,12 +175,11 @@ class UserFirstLoginView(PermissionsMixin, SessionWizardView): form.instance = self.request.user if isinstance(form, forms.UserMFAForm): - choices = form.fields["otp_level"].choices - if self.request.user.otp_force_enabled: + choices = form.fields["mfa_level"].choices + if self.request.user.mfa_force_enabled: choices = [(k, v) for k, v in choices if k == 2] else: choices = [(k, v) for k, v in choices if k in [0, 1]] - form.fields["otp_level"].choices = choices - form.fields["otp_level"].initial = self.request.user.otp_level - + form.fields["mfa_level"].choices = choices + form.fields["mfa_level"].initial = self.request.user.mfa_level return form diff --git a/jumpserver/jumpserver/apps/users/views/profile.py b/jumpserver/jumpserver/apps/users/views/profile.py new file mode 100644 index 0000000000000000000000000000000000000000..dc0359fa9427500c286d673785a5e816d2c65b9e --- /dev/null +++ b/jumpserver/jumpserver/apps/users/views/profile.py @@ -0,0 +1,272 @@ +# ~*~ coding: utf-8 ~*~ + +from __future__ import unicode_literals + + +from django.contrib.auth import authenticate +from django.core.cache import cache +from django.conf import settings +from django.http import HttpResponse +from django.shortcuts import redirect +from django.urls import reverse_lazy, reverse +from django.utils.translation import ugettext as _ +from django.views import View +from django.views.generic.base import TemplateView +from django.views.generic.edit import ( + UpdateView, FormView +) +from django.contrib.auth import logout as auth_logout + +from common.utils import get_logger, ssh_key_gen +from common.permissions import ( + PermissionsMixin, IsValidUser, + UserCanUpdatePassword, UserCanUpdateSSHKey, +) +from .. import forms +from ..models import User +from ..utils import ( + generate_otp_uri, check_otp_code, get_user_or_tmp_user, + delete_tmp_user_for_cache, check_password_rules, get_password_check_rules, +) + +__all__ = [ + 'UserProfileView', + 'UserProfileUpdateView', 'UserPasswordUpdateView', + 'UserPublicKeyUpdateView', 'UserPublicKeyGenerateView', + 'UserCheckPasswordView', 'UserOtpEnableInstallAppView', + 'UserOtpEnableBindView', 'UserOtpSettingsSuccessView', + 'UserDisableMFAView', 'UserOtpUpdateView', +] + +logger = get_logger(__name__) + + +class UserProfileView(PermissionsMixin, TemplateView): + template_name = 'users/user_profile.html' + permission_classes = [IsValidUser] + + def get_context_data(self, **kwargs): + mfa_setting = settings.SECURITY_MFA_AUTH + context = { + 'action': _('Profile'), + 'mfa_setting': mfa_setting if mfa_setting is not None else False, + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class UserProfileUpdateView(PermissionsMixin, UpdateView): + template_name = 'users/user_profile_update.html' + model = User + permission_classes = [IsValidUser] + form_class = forms.UserProfileForm + success_url = reverse_lazy('users:user-profile') + + def get_object(self, queryset=None): + return self.request.user + + def get_context_data(self, **kwargs): + context = { + 'app': _('User'), + 'action': _('Profile setting'), + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class UserPasswordUpdateView(PermissionsMixin, UpdateView): + template_name = 'users/user_password_update.html' + model = User + form_class = forms.UserPasswordForm + success_url = reverse_lazy('users:user-profile') + permission_classes = [IsValidUser, UserCanUpdatePassword] + + def get_object(self, queryset=None): + return self.request.user + + def get_context_data(self, **kwargs): + check_rules = get_password_check_rules() + context = { + 'app': _('Users'), + 'action': _('Password update'), + 'password_check_rules': check_rules, + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + def get_success_url(self): + auth_logout(self.request) + return super().get_success_url() + + def form_valid(self, form): + password = form.cleaned_data.get('new_password') + is_ok = check_password_rules(password) + if not is_ok: + form.add_error( + "new_password", + _("* Your password does not meet the requirements") + ) + return self.form_invalid(form) + return super().form_valid(form) + + +class UserPublicKeyUpdateView(PermissionsMixin, UpdateView): + template_name = 'users/user_pubkey_update.html' + model = User + form_class = forms.UserPublicKeyForm + permission_classes = [IsValidUser, UserCanUpdateSSHKey] + success_url = reverse_lazy('users:user-profile') + + def get_object(self, queryset=None): + return self.request.user + + def get_context_data(self, **kwargs): + context = { + 'app': _('Users'), + 'action': _('Public key update'), + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class UserPublicKeyGenerateView(PermissionsMixin, View): + permission_classes = [IsValidUser] + + def get(self, request, *args, **kwargs): + private, public = ssh_key_gen(username=request.user.username, hostname='jumpserver') + request.user.public_key = public + request.user.save() + response = HttpResponse(private, content_type='text/plain') + filename = "{0}-jumpserver.pem".format(request.user.username) + response['Content-Disposition'] = 'attachment; filename={}'.format(filename) + return response + + +class UserCheckPasswordView(FormView): + template_name = 'users/user_password_check.html' + form_class = forms.UserCheckPasswordForm + + def form_valid(self, form): + user = get_user_or_tmp_user(self.request) + password = form.cleaned_data.get('password') + user = authenticate(username=user.username, password=password) + if not user: + form.add_error("password", _("Password invalid")) + return self.form_invalid(form) + if not user.mfa_is_otp(): + user.enable_mfa() + user.save() + return redirect(self.get_success_url()) + + def get_success_url(self): + if settings.OTP_IN_RADIUS: + success_url = reverse_lazy('users:user-otp-settings-success') + else: + success_url = reverse('users:user-otp-enable-install-app') + return success_url + + def get_context_data(self, **kwargs): + context = { + 'user': get_user_or_tmp_user(self.request) + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class UserOtpEnableInstallAppView(TemplateView): + template_name = 'users/user_otp_enable_install_app.html' + + def get_context_data(self, **kwargs): + user = get_user_or_tmp_user(self.request) + context = { + 'user': user + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class UserOtpEnableBindView(TemplateView, FormView): + template_name = 'users/user_otp_enable_bind.html' + form_class = forms.UserCheckOtpCodeForm + success_url = reverse_lazy('users:user-otp-settings-success') + + def form_valid(self, form): + otp_code = form.cleaned_data.get('otp_code') + otp_secret_key = cache.get(self.request.session.session_key+'otp_key', '') + + if check_otp_code(otp_secret_key, otp_code): + self.save_otp(otp_secret_key) + return super().form_valid(form) + + else: + form.add_error("otp_code", _("MFA code invalid, or ntp sync server time")) + return self.form_invalid(form) + + def save_otp(self, otp_secret_key): + user = get_user_or_tmp_user(self.request) + user.enable_mfa() + user.otp_secret_key = otp_secret_key + user.save() + + def get_context_data(self, **kwargs): + user = get_user_or_tmp_user(self.request) + otp_uri, otp_secret_key = generate_otp_uri(self.request) + context = { + 'otp_uri': otp_uri, + 'otp_secret_key': otp_secret_key, + 'user': user + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + +class UserDisableMFAView(FormView): + template_name = 'users/user_disable_mfa.html' + form_class = forms.UserCheckOtpCodeForm + success_url = reverse_lazy('users:user-otp-settings-success') + + def form_valid(self, form): + user = self.request.user + otp_code = form.cleaned_data.get('otp_code') + + valid = user.check_mfa(otp_code) + if valid: + user.disable_mfa() + user.save() + return super().form_valid(form) + else: + form.add_error('otp_code', _('MFA code invalid, or ntp sync server time')) + return super().form_invalid(form) + + +class UserOtpUpdateView(UserDisableMFAView): + success_url = reverse_lazy('users:user-otp-enable-bind') + + +class UserOtpSettingsSuccessView(TemplateView): + template_name = 'flash_message_standalone.html' + + def get_context_data(self, **kwargs): + title, describe = self.get_title_describe() + context = { + 'title': title, + 'messages': describe, + 'interval': 1, + 'redirect_url': reverse('authentication:login'), + 'auto_redirect': True, + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + def get_title_describe(self): + user = get_user_or_tmp_user(self.request) + if self.request.user.is_authenticated: + auth_logout(self.request) + title = _('MFA enable success') + describe = _('MFA enable success, return login page') + if not user.mfa_enabled: + title = _('MFA disable success') + describe = _('MFA disable success, return login page') + delete_tmp_user_for_cache(self.request) + return title, describe + diff --git a/jumpserver/jumpserver/apps/users/views/user.py b/jumpserver/jumpserver/apps/users/views/user.py index 36922f47e565576fa97f7e59983d19a34c859622..8e6650240bec772012d34999a078ad80ce6d735f 100644 --- a/jumpserver/jumpserver/apps/users/views/user.py +++ b/jumpserver/jumpserver/apps/users/views/user.py @@ -4,48 +4,37 @@ from __future__ import unicode_literals from django.contrib import messages -from django.contrib.auth import authenticate from django.contrib.messages.views import SuccessMessageMixin from django.core.cache import cache -from django.conf import settings -from django.http import HttpResponse from django.shortcuts import redirect -from django.urls import reverse_lazy, reverse +from django.urls import reverse_lazy from django.utils.translation import ugettext as _ -from django.views import View from django.views.generic.base import TemplateView from django.views.generic.edit import ( - CreateView, UpdateView, FormView + CreateView, UpdateView ) from django.views.generic.detail import DetailView -from django.contrib.auth import logout as auth_logout from common.const import ( create_success_msg, update_success_msg, KEY_CACHE_RESOURCES_ID ) -from common.utils import get_logger, ssh_key_gen +from common.utils import get_logger from common.permissions import ( - PermissionsMixin, IsOrgAdmin, IsValidUser, - UserCanUpdatePassword, UserCanUpdateSSHKey, + PermissionsMixin, IsOrgAdmin, CanUpdateDeleteUser, ) from orgs.utils import current_org from .. import forms from ..models import User, UserGroup -from ..utils import generate_otp_uri, check_otp_code, \ - get_user_or_tmp_user, get_password_check_rules, check_password_rules, \ - is_need_unblock +from ..utils import get_password_check_rules, is_need_unblock from ..signals import post_user_create __all__ = [ 'UserListView', 'UserCreateView', 'UserDetailView', - 'UserUpdateView', 'UserGrantedAssetView', 'UserProfileView', - 'UserProfileUpdateView', 'UserPasswordUpdateView', - 'UserPublicKeyUpdateView', 'UserBulkUpdateView', - 'UserPublicKeyGenerateView', - 'UserOtpEnableAuthenticationView', 'UserOtpEnableInstallAppView', - 'UserOtpEnableBindView', 'UserOtpSettingsSuccessView', - 'UserOtpDisableAuthenticationView', 'UserOtpUpdateView' + 'UserUpdateView', 'UserBulkUpdateView', + 'UserGrantedAssetView', 'UserAssetPermissionListView', + 'UserGrantedRemoteAppView', 'UserRemoteAppPermissionListView', + 'UserGrantedDatabasesAppView', 'UserDatabaseAppPermissionListView', ] logger = get_logger(__name__) @@ -177,7 +166,7 @@ class UserBulkUpdateView(PermissionsMixin, TemplateView): class UserDetailView(PermissionsMixin, DetailView): model = User template_name = 'users/user_detail.html' - context_object_name = "user_object" + context_object_name = "object" key_prefix_block = "_LOGIN_BLOCK_{}" permission_classes = [IsOrgAdmin] @@ -221,234 +210,71 @@ class UserGrantedAssetView(PermissionsMixin, DetailView): return super().get_context_data(**kwargs) -class UserProfileView(PermissionsMixin, TemplateView): - template_name = 'users/user_profile.html' - permission_classes = [IsValidUser] - - def get_context_data(self, **kwargs): - mfa_setting = settings.SECURITY_MFA_AUTH - context = { - 'action': _('Profile'), - 'mfa_setting': mfa_setting if mfa_setting is not None else False, - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - -class UserProfileUpdateView(PermissionsMixin, UpdateView): - template_name = 'users/user_profile_update.html' +class UserAssetPermissionListView(PermissionsMixin, DetailView): model = User - permission_classes = [IsValidUser] - form_class = forms.UserProfileForm - success_url = reverse_lazy('users:user-profile') - - def get_object(self, queryset=None): - return self.request.user + template_name = 'users/user_asset_permission.html' + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { - 'app': _('User'), - 'action': _('Profile setting'), + 'app': _('Users'), + 'action': _('Asset permission'), } kwargs.update(context) return super().get_context_data(**kwargs) -class UserPasswordUpdateView(PermissionsMixin, UpdateView): - template_name = 'users/user_password_update.html' +class UserGrantedRemoteAppView(PermissionsMixin, DetailView): model = User - form_class = forms.UserPasswordForm - success_url = reverse_lazy('users:user-profile') - permission_classes = [IsValidUser, UserCanUpdatePassword] - - def get_object(self, queryset=None): - return self.request.user + template_name = 'users/user_granted_remote_app.html' + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): - check_rules = get_password_check_rules() context = { 'app': _('Users'), - 'action': _('Password update'), - 'password_check_rules': check_rules, + 'action': _('User granted RemoteApp'), } kwargs.update(context) return super().get_context_data(**kwargs) - def get_success_url(self): - auth_logout(self.request) - return super().get_success_url() - - def form_valid(self, form): - password = form.cleaned_data.get('new_password') - is_ok = check_password_rules(password) - if not is_ok: - form.add_error( - "new_password", - _("* Your password does not meet the requirements") - ) - return self.form_invalid(form) - return super().form_valid(form) - -class UserPublicKeyUpdateView(PermissionsMixin, UpdateView): - template_name = 'users/user_pubkey_update.html' +class UserRemoteAppPermissionListView(PermissionsMixin, DetailView): model = User - form_class = forms.UserPublicKeyForm - permission_classes = [IsValidUser, UserCanUpdateSSHKey] - success_url = reverse_lazy('users:user-profile') - - def get_object(self, queryset=None): - return self.request.user + template_name = 'users/user_remote_app_permission.html' + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): context = { 'app': _('Users'), - 'action': _('Public key update'), + 'action': _('RemoteApp permission'), } kwargs.update(context) return super().get_context_data(**kwargs) -class UserPublicKeyGenerateView(PermissionsMixin, View): - permission_classes = [IsValidUser] - - def get(self, request, *args, **kwargs): - private, public = ssh_key_gen(username=request.user.username, hostname='jumpserver') - request.user.public_key = public - request.user.save() - response = HttpResponse(private, content_type='text/plain') - filename = "{0}-jumpserver.pem".format(request.user.username) - response['Content-Disposition'] = 'attachment; filename={}'.format(filename) - return response - - -class UserOtpEnableAuthenticationView(FormView): - template_name = 'users/user_password_authentication.html' - form_class = forms.UserCheckPasswordForm - - def get_form(self, form_class=None): - user = get_user_or_tmp_user(self.request) - form = super().get_form(form_class=form_class) - form['username'].initial = user.username - return form - - def get_context_data(self, **kwargs): - user = get_user_or_tmp_user(self.request) - context = { - 'user': user - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - def form_valid(self, form): - user = get_user_or_tmp_user(self.request) - password = form.cleaned_data.get('password') - user = authenticate(username=user.username, password=password) - if not user: - form.add_error("password", _("Password invalid")) - return self.form_invalid(form) - return redirect(self.get_success_url()) - - def get_success_url(self): - return reverse('users:user-otp-enable-install-app') - - -class UserOtpEnableInstallAppView(TemplateView): - template_name = 'users/user_otp_enable_install_app.html' - - def get_context_data(self, **kwargs): - user = get_user_or_tmp_user(self.request) - context = { - 'user': user - } - kwargs.update(context) - return super().get_context_data(**kwargs) - - -class UserOtpEnableBindView(TemplateView, FormView): - template_name = 'users/user_otp_enable_bind.html' - form_class = forms.UserCheckOtpCodeForm - success_url = reverse_lazy('users:user-otp-settings-success') +class UserGrantedDatabasesAppView(PermissionsMixin, DetailView): + model = User + template_name = 'users/user_granted_database_app.html' + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): - user = get_user_or_tmp_user(self.request) - otp_uri, otp_secret_key = generate_otp_uri(self.request) context = { - 'otp_uri': otp_uri, - 'otp_secret_key': otp_secret_key, - 'user': user + 'app': _('Users'), + 'action': _('User granted DatabaseApp'), } kwargs.update(context) return super().get_context_data(**kwargs) - def form_valid(self, form): - otp_code = form.cleaned_data.get('otp_code') - otp_secret_key = cache.get(self.request.session.session_key+'otp_key', '') - - if check_otp_code(otp_secret_key, otp_code): - self.save_otp(otp_secret_key) - return super().form_valid(form) - else: - form.add_error("otp_code", _("MFA code invalid, or ntp sync server time")) - return self.form_invalid(form) - - def save_otp(self, otp_secret_key): - user = get_user_or_tmp_user(self.request) - user.enable_otp() - user.otp_secret_key = otp_secret_key - user.save() - - -class UserOtpDisableAuthenticationView(FormView): - template_name = 'users/user_otp_authentication.html' - form_class = forms.UserCheckOtpCodeForm - success_url = reverse_lazy('users:user-otp-settings-success') - - def form_valid(self, form): - user = self.request.user - otp_code = form.cleaned_data.get('otp_code') - otp_secret_key = user.otp_secret_key - - if check_otp_code(otp_secret_key, otp_code): - user.disable_otp() - user.save() - return super().form_valid(form) - else: - form.add_error('otp_code', _('MFA code invalid, or ntp sync server time')) - return super().form_invalid(form) - - -class UserOtpUpdateView(UserOtpDisableAuthenticationView): - success_url = reverse_lazy('users:user-otp-enable-bind') - - -class UserOtpSettingsSuccessView(TemplateView): - template_name = 'flash_message_standalone.html' - - # def get(self, request, *args, **kwargs): - # return super().get(request, *args, **kwargs) +class UserDatabaseAppPermissionListView(PermissionsMixin, DetailView): + model = User + template_name = 'users/user_database_app_permission.html' + permission_classes = [IsOrgAdmin] def get_context_data(self, **kwargs): - title, describe = self.get_title_describe() context = { - 'title': title, - 'messages': describe, - 'interval': 1, - 'redirect_url': reverse('authentication:login'), - 'auto_redirect': True, + 'app': _('Users'), + 'action': _('DatabaseApp permission'), } kwargs.update(context) return super().get_context_data(**kwargs) - - def get_title_describe(self): - user = get_user_or_tmp_user(self.request) - if self.request.user.is_authenticated: - auth_logout(self.request) - title = _('MFA enable success') - describe = _('MFA enable success, return login page') - if not user.otp_enabled: - title = _('MFA disable success') - describe = _('MFA disable success, return login page') - - return title, describe diff --git a/jumpserver/jumpserver/config_example.yml b/jumpserver/jumpserver/config_example.yml index 4b01fd10f2f0747a080390d91952e8114f4ebb58..321668a7fb4bd30018a5eef9ec97d30446ff5824 100644 --- a/jumpserver/jumpserver/config_example.yml +++ b/jumpserver/jumpserver/config_example.yml @@ -91,7 +91,6 @@ REDIS_PORT: 6379 # In order to perform this operation a successful bind must be completed on the connection # AUTH_LDAP_OPTIONS_OPT_REFERRALS: -1 - # OTP settings # OTP/MFA 配置 # OTP_VALID_WINDOW: 0 @@ -100,3 +99,12 @@ REDIS_PORT: 6379 # Perm show single asset to ungrouped node # 是否把未授权节点资产放入到 未分组 节点中 # PERM_SINGLE_ASSET_TO_UNGROUP_NODE: false +# +# 启用定时任务 +# PERIOD_TASK_ENABLE: True +# +# 启用二次复合认证配置 +# LOGIN_CONFIRM_ENABLE: False +# +# Windows 登录跳过手动输入密码 +# WINDOWS_SKIP_ALL_MANUAL_PASSWORD: False diff --git a/jumpserver/jumpserver/jms b/jumpserver/jumpserver/jms index 7d0c3d334c8574cf40e6205dc0da7b9dbfeb1fdc..fcbe189a896b31d67cff247c076fd68c3414d7f4 100755 --- a/jumpserver/jumpserver/jms +++ b/jumpserver/jumpserver/jms @@ -19,28 +19,23 @@ from daemon import pidfile BASE_DIR = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, BASE_DIR) +logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S") + try: from apps.jumpserver import const __version__ = const.VERSION except ImportError as e: - logging.info("Not found __version__: {}".format(e)) - logging.info("Sys path: {}".format(sys.path)) - logging.info("Python is: ") + print("Not found __version__: {}".format(e)) + print("Python is: ") logging.info(subprocess.call('which python', shell=True)) __version__ = 'Unknown' - try: - import apps - logging.info("List apps: {}".format(os.listdir('apps'))) - logging.info('apps is: {}'.format(apps)) - except: - pass + sys.exit(1) try: - from apps.jumpserver.conf import load_user_config - CONFIG = load_user_config() + from apps.jumpserver.const import CONFIG except ImportError as e: - logging.info("Import error: {}".format(e)) - logging.info("Could not find config file, `cp config_example.yml config.yml`") + print("Import error: {}".format(e)) + print("Could not find config file, `cp config_example.yml config.yml`") sys.exit(1) os.environ["PYTHONIOENCODING"] = "UTF-8" @@ -445,7 +440,10 @@ def stop_service(srv, sig=15): if process is None: print("\033[31m No process found\033[0m") continue - process.wait(1) + try: + process.wait(1) + except: + pass for i in range(STOP_TIMEOUT): if i == STOP_TIMEOUT - 1: print("\033[31m Error\033[0m") diff --git a/jumpserver/jumpserver/requirements/alpine_requirements.txt b/jumpserver/jumpserver/requirements/alpine_requirements.txt index 3eb144a0dd6acb173b85736d582b750711862eaf..47935bf967fc9533a6e6789aa5554c7ef8a0686b 100644 --- a/jumpserver/jumpserver/requirements/alpine_requirements.txt +++ b/jumpserver/jumpserver/requirements/alpine_requirements.txt @@ -1 +1,2 @@ -tiff-dev jpeg-dev zlib-dev freetype-dev lcms-dev libwebp-dev tcl-dev tk-dev python3-dev libressl-dev openldap-dev cyrus-sasl-dev krb5-dev sshpass postgresql-dev mariadb-dev sqlite-dev libffi-dev openssh-client gcc libc-dev linux-headers make autoconf +gcc make python3-dev python3 libffi-dev mariadb-dev libc-dev libffi-dev krb5-dev openldap-dev jpeg-dev linux-headers sshpass openssh-client + diff --git a/jumpserver/jumpserver/requirements/requirements.txt b/jumpserver/jumpserver/requirements/requirements.txt index d99c6ee272cafcd7df50a11e5281eacc0db22330..c3edaf18185471e3a34697f96a1d55a07f31ab71 100644 --- a/jumpserver/jumpserver/requirements/requirements.txt +++ b/jumpserver/jumpserver/requirements/requirements.txt @@ -59,7 +59,7 @@ pytz==2018.3 PyYAML==5.1 redis==2.10.6 requests==2.22.0 -jms-storage==0.0.25 +jms-storage==0.0.26 s3transfer==0.1.13 simplejson==3.13.2 six==1.11.0 @@ -89,3 +89,4 @@ flower==0.9.3 channels-redis==2.4.0 channels==2.3.0 daphne==2.3.0 +psutil==5.6.5 diff --git a/jumpserver/koko/Dockerfile b/jumpserver/koko/Dockerfile index 74796cf35c02077f1e376fd9c4ac9841083e8850..7a26078b34af8eb074bcf20869e1ad74c0e68501 100644 --- a/jumpserver/koko/Dockerfile +++ b/jumpserver/koko/Dockerfile @@ -4,6 +4,9 @@ WORKDIR /opt/koko ARG GOPROXY ENV GOPROXY=$GOPROXY ENV GO111MODULE=on +ENV GOOS=linux +ENV GOARCH=amd64 +ENV CGO_ENABLED=0 RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \ && apk update \ && apk add git @@ -12,23 +15,58 @@ RUN go mod download COPY . . RUN cd cmd && go build -ldflags "-X 'main.Buildstamp=`date -u '+%Y-%m-%d %I:%M:%S%p'`' -X 'main.Githash=`git rev-parse HEAD`' -X 'main.Goversion=`go version`'" -x -o koko koko.go -FROM alpine +FROM debian:stretch-slim +RUN apt-get update -y \ + && apt-get install -y --no-install-recommends gnupg dirmngr openssh-client procps curl \ + && rm -rf /var/lib/apt/lists/* + +ENV GOSU_VERSION 1.7 +RUN set -x \ + && apt-get update && apt-get install -y --no-install-recommends ca-certificates wget && rm -rf /var/lib/apt/lists/* \ + && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$(dpkg --print-architecture)" \ + && wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$(dpkg --print-architecture).asc" \ + && export GNUPGHOME="$(mktemp -d)" \ + && ( gpg --batch --keyserver p80.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4 \ + || gpg --batch --keyserver hkps.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4 \ + || gpg --batch --keyserver keyserver.ubuntu.com --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4 \ + || gpg --batch --keyserver pgp.mit.edu --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4 \ + || gpg --batch --keyserver keyserver.pgp.com --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4 ) \ + && gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu \ + && gpgconf --kill all \ + && rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc \ + && chmod +x /usr/local/bin/gosu \ + && gosu nobody true \ + && apt-get purge -y --auto-remove ca-certificates wget + +RUN set -ex; \ +# gpg: key 5072E1F5: public key "MySQL Release Engineering " imported + key='A4A9406876FCBD3C456770C88C718D3B5072E1F5'; \ + export GNUPGHOME="$(mktemp -d)"; \ + ( gpg --batch --keyserver p80.pool.sks-keyservers.net --recv-keys "$key" \ + || gpg --batch --keyserver hkps.pool.sks-keyservers.net --recv-keys "$key" \ + || gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" \ + || gpg --batch --keyserver pgp.mit.edu --recv-keys "$key" \ + || gpg --batch --keyserver keyserver.pgp.com --recv-keys "$key" ); \ + gpg --batch --export "$key" > /etc/apt/trusted.gpg.d/mysql.gpg; \ + gpgconf --kill all; \ + rm -rf "$GNUPGHOME"; \ + apt-key list > /dev/null + +ENV MYSQL_MAJOR 8.0 +ENV MYSQL_VERSION 8.0.19-1debian9 +RUN echo "deb http://repo.mysql.com/apt/debian/ stretch mysql-${MYSQL_MAJOR}" > /etc/apt/sources.list.d/mysql.list +RUN apt-get update && apt-get install -y mysql-community-client="${MYSQL_VERSION}" && rm -rf /var/lib/apt/lists/* + +ENV TZ Asia/Shanghai WORKDIR /opt/koko/ COPY --from=stage-build /opt/koko/cmd/koko . COPY --from=stage-build /opt/koko/cmd/locale/ locale COPY --from=stage-build /opt/koko/cmd/static/ static COPY --from=stage-build /opt/koko/cmd/templates/ templates -COPY cmd/config_example.yml . -COPY entrypoint.sh . -RUN chmod 755 ./entrypoint.sh \ - && sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \ - && apk update \ - && apk add -U tzdata \ - && apk add curl \ - && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ - && echo "Asia/Shanghai" > /etc/timezone \ - && apk del tzdata \ - && rm -rf /var/cache/apk/* +COPY --from=stage-build /opt/koko/cmd/config_example.yml . +COPY --from=stage-build /opt/koko/entrypoint.sh . + +RUN chmod 755 entrypoint.sh EXPOSE 2222 5000 CMD ["./entrypoint.sh"] diff --git a/jumpserver/koko/LICENSE b/jumpserver/koko/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..f288702d2fa16d3cdf0035b15a9fcbc552cd88e7 --- /dev/null +++ b/jumpserver/koko/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/jumpserver/koko/Makefile b/jumpserver/koko/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..da9830bb05b8de613835fda49310276ff04d5127 --- /dev/null +++ b/jumpserver/koko/Makefile @@ -0,0 +1,44 @@ +BRANCH := $(shell git symbolic-ref HEAD 2>/dev/null | cut -d"/" -f 3) +BUILD := $(shell git rev-parse --short HEAD) +VERSION = $(BRANCH)-$(BUILD) +BASEPATH := $(shell pwd) + +NAME := koko +SOFTWARENAME:=$(NAME)-$(VERSION) +BUILDDIR:=$(BASEPATH)/build +DIRNAME := kokodir +KOKOSRCFILE:= $(BASEPATH)/cmd/koko.go +VERSIONFLAGS="-X 'main.Buildstamp=`date -u '+%Y-%m-%d %I:%M:%S%p'`' -X 'main.Githash=`git rev-parse HEAD`' -X 'main.Goversion=`go version`'" +PLATFORMS := linux darwin + +.PHONY: release +release: linux darwin Asset + @echo "编译完成" + rm -rf $(BUILDDIR)/$(DIRNAME) + ls $(BUILDDIR)/koko* + +.PHONY: Asset +Asset: + @[ -d $(BUILDDIR) ] || mkdir -p $(BUILDDIR) + @[ -d $(BUILDDIR)/$(DIRNAME) ] || mkdir -p $(BUILDDIR)/$(DIRNAME) + cp -r $(BASEPATH)/cmd/locale $(BUILDDIR)/$(DIRNAME) + cp -r $(BASEPATH)/cmd/static $(BUILDDIR)/$(DIRNAME) + cp -r $(BASEPATH)/cmd/templates $(BUILDDIR)/$(DIRNAME) + cp -r $(BASEPATH)/cmd/config_example.yml $(BUILDDIR)/$(DIRNAME) + +.PHONY: $(PLATFORMS) +$(PLATFORMS): Asset + @echo "编译" $@ + CGO_ENABLED=0 GOOS=$@ GOARCH=amd64 go build -ldflags $(VERSIONFLAGS) -x -o $(BUILDDIR)/$(NAME)-$@ $(KOKOSRCFILE) + cp $(BUILDDIR)/$(NAME)-$@ $(BUILDDIR)/$(DIRNAME)/$(NAME) + tar czvf $(BUILDDIR)/$(SOFTWARENAME)-$@-amd64.tar.gz -C $(BUILDDIR) $(DIRNAME) + rm $(BUILDDIR)/$(NAME)-$@ + +.PHONY: docker +docker: + @echo "build docker images" + docker build -t koko --build-arg GOPROXY=$(GOPROXY) . + +.PHONY: clean +clean: + -rm -rf $(BUILDDIR) \ No newline at end of file diff --git a/jumpserver/koko/README.md b/jumpserver/koko/README.md index 8de33dc227500facf80ea10b4d5b4e1ac2e2c9f3..177d0692bdc1d9bdc0c4bbc69f5dc9ae7aa89187 100644 --- a/jumpserver/koko/README.md +++ b/jumpserver/koko/README.md @@ -23,15 +23,12 @@ go get github.com/jumpserver/koko 2.编译应用 -先进入cmd文件夹, 并构建应用. -```shell -cd cmd -``` +在 koko 项目下构建应用. ```shell make linux ``` > 如果构建成功,会在项目下自动生成build文件夹,里面包含当前分支的linux 64位版本压缩包. -因为使用go mod进行依赖管理,可以设置GOPROXY=https://goproxy.io代理下载部分依赖包。 +因为使用go mod进行依赖管理,可以设置环境变量 GOPROXY=https://goproxy.io 代理下载部分依赖包。 ## 使用 @@ -56,10 +53,6 @@ cd kokodir ## 构建docker镜像 -进入cmd文件夹 -```shell -cd cmd -``` ```shell make docker ``` diff --git a/jumpserver/koko/README.schbrain.md b/jumpserver/koko/README.schbrain.md new file mode 100644 index 0000000000000000000000000000000000000000..c9416e4ed56347bcb49895a615681fc516f34996 --- /dev/null +++ b/jumpserver/koko/README.schbrain.md @@ -0,0 +1 @@ +要把golang mod 映射到github mod,用镜像, go.mod文件改了 diff --git a/jumpserver/koko/cmd/Makefile b/jumpserver/koko/cmd/Makefile deleted file mode 100644 index e0c108bbeab99d5bef5382f1f55b0c0624178d57..0000000000000000000000000000000000000000 --- a/jumpserver/koko/cmd/Makefile +++ /dev/null @@ -1,48 +0,0 @@ - -BRANCH := $(shell git symbolic-ref HEAD 2>/dev/null | cut -d"/" -f 3) -BUILD := $(shell git rev-parse --short HEAD) -VERSION = $(BRANCH)-$(BUILD) - -NAME := koko -DIRNAME := kokodir -BASEPATH := $(shell pwd) -CGO_ENABLED = 0 -GOCMD = go -GOBUILD = $(GOCMD) build - -SOFTWARENAME=$(NAME)-$(VERSION) -KOKOSRCFILE= koko.go -BUILDDIR:=$(BASEPATH)/../build -ASSETS=locale static templates config_example.yml -GOFLAGS="-X 'main.Buildstamp=`date -u '+%Y-%m-%d %I:%M:%S%p'`' -X 'main.Githash=`git rev-parse HEAD`' -X 'main.Goversion=`go version`'" -PLATFORMS := linux darwin - -.PHONY: release -release: linux darwin - - -.PHONY:Asset -Asset: - @[ -d $(BUILDDIR) ] || mkdir -p $(BUILDDIR) - @[ -d $(DIRNAME) ] || mkdir -p $(DIRNAME) - cp -r $(ASSETS) $(DIRNAME) - -.PHONY: $(PLATFORMS) -$(PLATFORMS): Asset - @echo "编译" $@ - GOOS=$@ GOARCH=amd64 go build -ldflags $(GOFLAGS) -x -o $(NAME) $(KOKOSRCFILE) - cp -f $(NAME) $(DIRNAME) - tar czvf $(BUILDDIR)/$(SOFTWARENAME)-$@-amd64.tar.gz $(DIRNAME) - -.PHONY: docker -docker: - @echo "build docker images" - docker build -t koko --build-arg GOPROXY=$(GOPROXY) $(BASEPATH)/../ - -.PHONY: clean -clean: - -rm -rf $(NAME) - -rm -rf $(DIRNAME) - -rm -rf $(BUILDDIR) - - diff --git a/jumpserver/koko/cmd/locale/en_US/LC_MESSAGES/koko.po b/jumpserver/koko/cmd/locale/en_US/LC_MESSAGES/koko.po index 295caef0d351a67b84e0915d96838962e32bb00c..fd88da4bcd645de7190a3be2ae0fee7dda7d97b9 100644 --- a/jumpserver/koko/cmd/locale/en_US/LC_MESSAGES/koko.po +++ b/jumpserver/koko/cmd/locale/en_US/LC_MESSAGES/koko.po @@ -19,37 +19,37 @@ msgstr "" #. i18n.T #: pkg/handler/banner.go:48 -msgid "directly login" +msgid "part IP, Hostname, Comment" msgstr "" #. i18n.T -#: pkg/handler/banner.go:49 -msgid "part IP, Hostname, Comment" +#: pkg/handler/banner.go:48 +msgid "to search login if unique" msgstr "" #. i18n.T #: pkg/handler/banner.go:49 -msgid "to search login if unique" +msgid "/ + IP, Hostname, Comment" msgstr "" #. i18n.T -#: pkg/handler/banner.go:50 -msgid "/ + IP, Hostname, Comment" +#: pkg/handler/banner.go:49 +msgid "to search, such as: /192.168" msgstr "" #. i18n.T #: pkg/handler/banner.go:50 -msgid "to search, such as: /192.168" +msgid "display the host you have permission" msgstr "" #. i18n.T #: pkg/handler/banner.go:51 -msgid "display the host you have permission" +msgid "display the node that you have permission" msgstr "" #. i18n.T #: pkg/handler/banner.go:52 -msgid "display the node that you have permission" +msgid "display the databases that you have permission" msgstr "" #. i18n.T @@ -100,15 +100,13 @@ msgstr "" #. i18n.T #: pkg/handler/banner.go:96 msgid "" -"\n" -"Tips: Enter the asset ID and directly login the asset.\n" +"Enter ID number directly login the asset, multiple search use // + field, " +"such as: //16" msgstr "" #. i18n.T #: pkg/handler/banner.go:97 -msgid "" -"\n" -"Page up: P/p\tPage down: Enter|N/n\tBACK: b.\n" +msgid "Page up: b\tPage down: n" msgstr "" #. i18n.T @@ -147,42 +145,104 @@ msgid "Username" msgstr "" #. i18n.T -#: pkg/proxy/parser.go:131 +#: pkg/handler/banner.go:105 +msgid "all" +msgstr "" + +#. i18n.T +#: pkg/handler/banner.go:106 +msgid "Search: %s" +msgstr "" + +#. i18n.T +#: pkg/handler/banner.go:107 +msgid "DBType" +msgstr "" + +#. i18n.T +#: pkg/handler/banner.go:108 +msgid "DB Name" +msgstr "" + +#. i18n.T +#: pkg/handler/banner.go:109 +msgid "No Databases" +msgstr "" + +#. i18n.T +#: pkg/handler/banner.go:110 +msgid "" +"Enter ID number directly login the database, multiple search use // + field, " +"such as: //16" +msgstr "" + +#. i18n.T +#: pkg/proxy/dbproxy.go:112 +msgid "Database connecting to %s %.1f" +msgstr "" + +#. i18n.T +#: pkg/proxy/dbproxy.go:131 +msgid "System user <%s> and database <%s> protocol are inconsistent." +msgstr "" + +#. i18n.T +#: pkg/proxy/dbproxy.go:137 +msgid "Database %s protocol client not installed." +msgstr "" + +#. i18n.T +#: pkg/proxy/dbswitch.go:148 +msgid "Database connect idle more than %d minutes, disconnect" +msgstr "" + +#. i18n.T +#: pkg/proxy/dbswitch.go:155 +msgid "Database connection terminated by administrator" +msgstr "" + +#. i18n.T +#: pkg/proxy/parser.go:140 msgid "Command `%s` is forbidden" msgstr "" #. i18n.T -#: pkg/proxy/proxy.go:143 +#: pkg/proxy/proxy.go:146 msgid "Reuse SSH connections (%s@%s) [Number of connections: %d]" msgstr "" #. i18n.T -#: pkg/proxy/proxy.go:161 +#: pkg/proxy/proxy.go:164 msgid "Connecting to %s@%s %.1f" msgstr "" #. i18n.T -#: pkg/proxy/proxy.go:180 +#: pkg/proxy/proxy.go:183 msgid "System user <%s> and asset <%s> protocol are inconsistent." msgstr "" #. i18n.T -#: pkg/proxy/proxy.go:186 +#: pkg/proxy/proxy.go:189 msgid "" "Terminal only support protocol ssh/telnet, please use web terminal to access" msgstr "" #. i18n.T -#: pkg/proxy/sessmanager.go:67 +#: pkg/proxy/sessmanager.go:75 msgid "Connect with api server failed" msgstr "" #. i18n.T -#: pkg/proxy/switch.go:159 +#: pkg/proxy/sessmanager.go:117 +msgid "Create database session failed" +msgstr "" + +#. i18n.T +#: pkg/proxy/switch.go:173 msgid "Connect idle more than %d minutes, disconnect" msgstr "" #. i18n.T -#: pkg/proxy/switch.go:166 +#: pkg/proxy/switch.go:180 msgid "Terminated by administrator" msgstr "" diff --git a/jumpserver/koko/cmd/locale/zh_CN/LC_MESSAGES/koko.mo b/jumpserver/koko/cmd/locale/zh_CN/LC_MESSAGES/koko.mo index f3f6dda5a260137e6358f0ecddc60457ff7b3162..a9281031208a84262f6a7d8b5a57123105b0e96e 100644 Binary files a/jumpserver/koko/cmd/locale/zh_CN/LC_MESSAGES/koko.mo and b/jumpserver/koko/cmd/locale/zh_CN/LC_MESSAGES/koko.mo differ diff --git a/jumpserver/koko/cmd/locale/zh_CN/LC_MESSAGES/koko.po b/jumpserver/koko/cmd/locale/zh_CN/LC_MESSAGES/koko.po index c70294fa15d79754e141fac006ca3229a1e4c5e2..7666c782ea8e8f7493904540ee415c18f6afedb0 100644 --- a/jumpserver/koko/cmd/locale/zh_CN/LC_MESSAGES/koko.po +++ b/jumpserver/koko/cmd/locale/zh_CN/LC_MESSAGES/koko.po @@ -19,39 +19,42 @@ msgstr "欢迎使用Jumpserver开源堡垒机系统" #. i18n.T #: pkg/handler/banner.go:48 -msgid "directly login" -msgstr "直接登录" - -#. i18n.T -#: pkg/handler/banner.go:49 msgid "part IP, Hostname, Comment" msgstr "部分IP、主机名、备注" #. i18n.T -#: pkg/handler/banner.go:49 +#: pkg/handler/banner.go:48 +#, fuzzy msgid "to search login if unique" msgstr "搜索登录(如果唯一)" #. i18n.T -#: pkg/handler/banner.go:50 +#: pkg/handler/banner.go:49 msgid "/ + IP, Hostname, Comment" msgstr "/ + IP,主机名 or 备注" #. i18n.T -#: pkg/handler/banner.go:50 +#: pkg/handler/banner.go:49 +#, fuzzy msgid "to search, such as: /192.168" msgstr "搜索,如:/192.168" #. i18n.T -#: pkg/handler/banner.go:51 +#: pkg/handler/banner.go:50 msgid "display the host you have permission" msgstr "显示您有权限的主机" #. i18n.T -#: pkg/handler/banner.go:52 +#: pkg/handler/banner.go:51 msgid "display the node that you have permission" msgstr "显示您有权限的节点" +#. i18n.T +#: pkg/handler/banner.go:52 +#, fuzzy +msgid "display the databases that you have permission" +msgstr "显示您有权限的数据库" + #. i18n.T #: pkg/handler/banner.go:53 msgid "refresh your assets and nodes" @@ -101,20 +104,15 @@ msgstr "没有资产" #: pkg/handler/banner.go:96 #, fuzzy msgid "" -"\n" -"Tips: Enter the asset ID and directly login the asset.\n" -msgstr "" -"\n" -"提示:输入资产ID,登录资产\n" +"Enter ID number directly login the asset, multiple search use // + field, " +"such as: //16" +msgstr "提示:输入资产ID直接登录,二级搜索使用 // + 字段,如://192" #. i18n.T #: pkg/handler/banner.go:97 -msgid "" -"\n" -"Page up: P/p\tPage down: Enter|N/n\tBACK: b.\n" -msgstr "" -"\n" -"上一页:P/p 下一页:Enter|N/n 返回:B/b\n" +#, fuzzy +msgid "Page up: b\tPage down: n" +msgstr "上一页:b 下一页:n" #. i18n.T #: pkg/handler/banner.go:98 @@ -156,42 +154,111 @@ msgid "Username" msgstr "用户名" #. i18n.T -#: pkg/proxy/parser.go:131 +#: pkg/handler/banner.go:105 +msgid "all" +msgstr "所有" + +#. i18n.T +#: pkg/handler/banner.go:106 +#, fuzzy +msgid "Search: %s" +msgstr "搜索: %s" + +#. i18n.T +#: pkg/handler/banner.go:107 +msgid "DBType" +msgstr "数据库类型" + +#. i18n.T +#: pkg/handler/banner.go:108 +#, fuzzy +msgid "DB Name" +msgstr "数据库名称" + +#. i18n.T +#: pkg/handler/banner.go:109 +msgid "No Databases" +msgstr "无数据库" + +#. i18n.T +#: pkg/handler/banner.go:110 +#, fuzzy +msgid "" +"Enter ID number directly login the database, multiple search use // + field, " +"such as: //16" +msgstr "提示:输入数据库ID直接登录,二级搜索使用 // + 字段,如://192" + +#. i18n.T +#: pkg/proxy/dbproxy.go:112 +#, fuzzy +msgid "Database connecting to %s %.1f" +msgstr "连接数据库 %s %.1f" + +#. i18n.T +#: pkg/proxy/dbproxy.go:131 +#, fuzzy +msgid "System user <%s> and database <%s> protocol are inconsistent." +msgstr "系统用户<%s>和资产<%s>协议不一致" + +#. i18n.T +#: pkg/proxy/dbproxy.go:137 +msgid "Database %s protocol client not installed." +msgstr "%s 协议的数据库客户端未安装" + +#. i18n.T +#: pkg/proxy/dbswitch.go:148 +#, fuzzy +msgid "Database connect idle more than %d minutes, disconnect" +msgstr "数据库连接空闲时间超过 %d 分钟,断开连接" + +#. i18n.T +#: pkg/proxy/dbswitch.go:155 +#, fuzzy +msgid "Database connection terminated by administrator" +msgstr "管理员中断数据库连接" + +#. i18n.T +#: pkg/proxy/parser.go:140 msgid "Command `%s` is forbidden" msgstr "命令 `%s` 是被禁止的 ..." #. i18n.T -#: pkg/proxy/proxy.go:143 +#: pkg/proxy/proxy.go:146 msgid "Reuse SSH connections (%s@%s) [Number of connections: %d]" msgstr "复用SSH连接(%s@%s)[连接数量: %d]" #. i18n.T -#: pkg/proxy/proxy.go:161 +#: pkg/proxy/proxy.go:164 msgid "Connecting to %s@%s %.1f" msgstr "开始连接到 %s@%s %.1f" #. i18n.T -#: pkg/proxy/proxy.go:180 +#: pkg/proxy/proxy.go:183 msgid "System user <%s> and asset <%s> protocol are inconsistent." msgstr "系统用户<%s>和资产<%s>协议不一致" #. i18n.T -#: pkg/proxy/proxy.go:186 +#: pkg/proxy/proxy.go:189 msgid "" "Terminal only support protocol ssh/telnet, please use web terminal to access" msgstr "终端仅支持ssh/telnet协议,请使用web终端登录" #. i18n.T -#: pkg/proxy/sessmanager.go:67 +#: pkg/proxy/sessmanager.go:75 msgid "Connect with api server failed" msgstr "连接API服务失败" #. i18n.T -#: pkg/proxy/switch.go:159 +#: pkg/proxy/sessmanager.go:117 +msgid "Create database session failed" +msgstr "创建数据库会话失败" + +#. i18n.T +#: pkg/proxy/switch.go:173 msgid "Connect idle more than %d minutes, disconnect" msgstr "空闲时间超过%d分钟,断开连接" #. i18n.T -#: pkg/proxy/switch.go:166 +#: pkg/proxy/switch.go:180 msgid "Terminated by administrator" msgstr "管理员中断连接" diff --git a/jumpserver/koko/cmd/static/plugins/elfinder/elfinder.full.js b/jumpserver/koko/cmd/static/plugins/elfinder/elfinder.full.js index 44c0048d2472e726154073262bb30579798d3724..53b61918d9caff19f0ab98c53e90f3bb58630aa6 100755 --- a/jumpserver/koko/cmd/static/plugins/elfinder/elfinder.full.js +++ b/jumpserver/koko/cmd/static/plugins/elfinder/elfinder.full.js @@ -19695,49 +19695,49 @@ $.fn.elfindersearchbutton = function(cmd) { }); }) .one('open', function() { - opts = (fm.api < 2.1)? null : $('
    ') - .append( - $('
    ') - .append( - $(''), - $(''), - $('') - ), - $('
    ') - .append( - $('') - ) - ) - .hide() - .appendTo(fm.getUI()); - if (opts) { - if (sTypes) { - typeSet = opts.find('.elfinder-search-type'); - $.each(cmd.options.searchTypes, function(i, v) { - typeSet.append($('')); - }); - } - opts.find('div.buttonset').buttonset(); - $('#'+id('SearchFromAll')).next('label').attr('title', fm.i18n('searchTarget', fm.i18n('btnAll'))); - if (sTypes) { - $.each(sTypes, function(i, v) { - if (v.title) { - $('#'+id(i)).next('label').attr('title', fm.i18n(v.title)); - } - }); - } - opts.on('mousedown', 'div.buttonset', function(e){ - e.stopPropagation(); - opts.data('infocus', true); - }) - .on('click', 'input', function(e) { - e.stopPropagation(); - $.trim(input.val())? search() : input.trigger('focus'); - }) - .on('close', function() { - input.trigger('blur'); - }); - } + // opts = (fm.api < 2.1)? null : $('
    ') + // .append( + // $('
    ') + // .append( + // $(''), + // $(''), + // $('') + // ), + // $('
    ') + // .append( + // $('') + // ) + // ) + // .hide() + // .appendTo(fm.getUI()); + // if (opts) { + // if (sTypes) { + // typeSet = opts.find('.elfinder-search-type'); + // $.each(cmd.options.searchTypes, function(i, v) { + // typeSet.append($('')); + // }); + // } + // opts.find('div.buttonset').buttonset(); + // $('#'+id('SearchFromAll')).next('label').attr('title', fm.i18n('searchTarget', fm.i18n('btnAll'))); + // if (sTypes) { + // $.each(sTypes, function(i, v) { + // if (v.title) { + // $('#'+id(i)).next('label').attr('title', fm.i18n(v.title)); + // } + // }); + // } + // opts.on('mousedown', 'div.buttonset', function(e){ + // e.stopPropagation(); + // opts.data('infocus', true); + // }) + // .on('click', 'input', function(e) { + // e.stopPropagation(); + // $.trim(input.val())? search() : input.trigger('focus'); + // }) + // .on('close', function() { + // input.trigger('blur'); + // }); + // } }) .bind('searchend', function() { input.val(''); diff --git a/jumpserver/koko/cmd/templates/elfinder/file_manager.html b/jumpserver/koko/cmd/templates/elfinder/file_manager.html index c9f7cf6d207d4e11608c0fe8400962a2891f7559..0bbd30e84cea174f2b4963c4b4b4c6641a7bbcc4 100644 --- a/jumpserver/koko/cmd/templates/elfinder/file_manager.html +++ b/jumpserver/koko/cmd/templates/elfinder/file_manager.html @@ -51,7 +51,10 @@ ['copy', 'cut', 'paste'], ['rm'], ['rename'], - ['view'] + ['view'], + {{if eq . "_"}} + ['search'] + {{end}} ], cwd : {oldSchool: true} }, diff --git a/jumpserver/koko/go.mod b/jumpserver/koko/go.mod index 3c3c9f5ba6c5cd5e203a86c8b403249ef5e1fa4a..ac0744dde3fc78ce84d70447d566f28dfba44867 100644 --- a/jumpserver/koko/go.mod +++ b/jumpserver/koko/go.mod @@ -6,12 +6,13 @@ require ( github.com/Azure/azure-pipeline-go v0.1.9 // indirect github.com/Azure/azure-storage-blob-go v0.6.0 github.com/BurntSushi/toml v0.3.1 // indirect - github.com/LeeEirc/elfinder v0.0.8 + github.com/LeeEirc/elfinder v0.0.11 github.com/aliyun/aliyun-oss-go-sdk v1.9.8 github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect github.com/aws/aws-sdk-go v1.19.46 github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect - github.com/elastic/go-elasticsearch v0.0.0 + github.com/creack/pty v1.1.9 + github.com/elastic/go-elasticsearch/v6 v6.8.5 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect github.com/gliderlabs/ssh v0.2.3-0.20190711180243-866d0ddf7991 github.com/gorilla/mux v1.7.2 @@ -42,4 +43,13 @@ replace ( github.com/gliderlabs/ssh v0.2.3-0.20190711180243-866d0ddf7991 => github.com/ibuler/ssh v0.1.6-0.20191022095544-d805cc9f27a8 github.com/pkg/sftp v1.10.0 => github.com/LeeEirc/sftp v1.10.2 golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 => github.com/ibuler/crypto v0.0.0-20190715092645-911d13b3bf6e + golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 => github.com/golang/text v0.3.1-0.20180807135948-17ff2d5776d2 + golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 => github.com/golang/time v0.0.0-20190308202827-9d24e82272b4 + golang.org/x/sys v0.0.0-20190422165155-953cdadca894 => github.com/golang/sys v0.0.0-20190422165155-953cdadca894 + golang.org/x/sys v0.0.0-20190412213103-97732733099d => github.com/golang/sys v0.0.0-20190412213103-97732733099d + golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 => github.com/golang/net v0.0.0-20190404232315-eb5bcb51f2a3 + golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 => github.com/ibuler/crypto v0.0.0-20181203042331-505ab145d0a9 + golang.org/x/text v0.3.0 => github.com/golang/text v0.3.0 + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 => github.com/ibuler/crypto v0.0.0-20190308221718-c2843e01d9a2 + golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a => github.com/golang/sys v0.0.0-20190215142949-d0b11bdaac8a ) diff --git a/jumpserver/koko/go.sum b/jumpserver/koko/go.sum index 6de1cee99b890eb1188ba485053529c9e43bd860..00a80e47938a37c27b034232061ca36d3dd862c9 100644 --- a/jumpserver/koko/go.sum +++ b/jumpserver/koko/go.sum @@ -5,8 +5,8 @@ github.com/Azure/azure-storage-blob-go v0.6.0 h1:SEATKb3LIHcaSIX+E6/K4kJpwfuozFE github.com/Azure/azure-storage-blob-go v0.6.0/go.mod h1:oGfmITT1V6x//CswqY2gtAHND+xIP64/qL7a5QJix0Y= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/LeeEirc/elfinder v0.0.8 h1:y1BSXvtRve6WwMiSYyynY+oEbH+rWMuScoZ1YU67H4E= -github.com/LeeEirc/elfinder v0.0.8/go.mod h1:d1bMAAydkZSBxSN/EuQjBg6B0xcPP3boHuYEpzEHYTs= +github.com/LeeEirc/elfinder v0.0.11 h1:LP+53Q0V2WhxTqR720X7B8rkkX2YDq47dSIGLR1xA9s= +github.com/LeeEirc/elfinder v0.0.11/go.mod h1:d1bMAAydkZSBxSN/EuQjBg6B0xcPP3boHuYEpzEHYTs= github.com/LeeEirc/sftp v1.10.2 h1:SGpj84RbStlwH+ThXYUsxtxtbzAzpUY8z5gQN4p12OI= github.com/LeeEirc/sftp v1.10.2/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/aliyun/aliyun-oss-go-sdk v1.9.8 h1:BOflvK0Zs/zGmoabyFIzTg5c3kguktWTXEwewwbuba0= @@ -17,11 +17,13 @@ github.com/aws/aws-sdk-go v1.19.46 h1:lRqljzjkGmEeiawkw4z1QgtCnU/S5Jw8lNeUuvmydU github.com/aws/aws-sdk-go v1.19.46/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7On9kyUzm7fvRZumSyy/IUiSC7AzL0I1jKKtwooA= github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= +github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/elastic/go-elasticsearch v0.0.0 h1:Pd5fqOuBxKxv83b0+xOAJDAkziWYwFinWnBO0y+TZaA= -github.com/elastic/go-elasticsearch v0.0.0/go.mod h1:TkBSJBuTyFdBnrNqoPc54FN0vKf5c04IdM4zuStJ7xg= +github.com/elastic/go-elasticsearch/v6 v6.8.5 h1:U2HtkBseC1FNBmDr0TR2tKltL6FxoY+niDAlj5M8TK8= +github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/go-playground/form v3.1.4+incompatible h1:lvKiHVxE2WvzDIoyMnWcjyiBxKt2+uFJyZcPYWsLnjI= diff --git a/jumpserver/koko/pkg/auth/server.go b/jumpserver/koko/pkg/auth/server.go index c82c450f3cbbb6bb7819899b8d4529a0cedad509..2cc14dee45753b281c5c8f77d64c8bf01426d0d6 100644 --- a/jumpserver/koko/pkg/auth/server.go +++ b/jumpserver/koko/pkg/auth/server.go @@ -2,20 +2,24 @@ package auth import ( "net" + "strings" "github.com/gliderlabs/ssh" gossh "golang.org/x/crypto/ssh" - "github.com/jumpserver/koko/pkg/cctx" "github.com/jumpserver/koko/pkg/common" "github.com/jumpserver/koko/pkg/config" "github.com/jumpserver/koko/pkg/logger" + "github.com/jumpserver/koko/pkg/model" "github.com/jumpserver/koko/pkg/service" ) var mfaInstruction = "Please enter 6 digits." var mfaQuestion = "[MFA auth]: " +var confirmInstruction = "Please wait for your admin to confirm." +var confirmQuestion = "Do you want to continue [Y/n]? : " + const ( actionAccepted = "Accepted" actionFailed = "Failed" @@ -31,28 +35,33 @@ func checkAuth(ctx ssh.Context, password, publicKey string) (res ssh.AuthResult) authMethod = "password" } remoteAddr, _, _ := net.SplitHostPort(ctx.RemoteAddr().String()) - - resp, err := service.Authenticate(username, password, publicKey, remoteAddr, "T") - if err != nil { - action = actionFailed - logger.Infof("%s %s for %s from %s", action, authMethod, username, remoteAddr) - return + userClient, ok := ctx.Value(model.ContextKeyClient).(*service.SessionClient) + if !ok { + sessionClient := service.NewSessionClient(service.Username(username), + service.RemoteAddr(remoteAddr), service.LoginType("T")) + userClient = &sessionClient + ctx.SetValue(model.ContextKeyClient, userClient) } - if resp != nil && resp.User != nil { - switch resp.User.OTPLevel { - case 0: - res = ssh.AuthSuccessful - case 1, 2: - action = actionPartialAccepted - res = ssh.AuthPartiallySuccessful - default: - } - ctx.SetValue(cctx.ContextKeyUser, resp.User) - ctx.SetValue(cctx.ContextKeySeed, resp.Seed) - ctx.SetValue(cctx.ContextKeyToken, resp.Token) + userClient.SetOption(service.Password(password), service.PublicKey(publicKey)) + user, authStatus := userClient.Authenticate(ctx) + switch authStatus { + case service.AuthMFARequired: + action = actionPartialAccepted + res = ssh.AuthPartiallySuccessful + case service.AuthSuccess: + res = ssh.AuthSuccessful + ctx.SetValue(model.ContextKeyUser, &user) + case service.AuthConfirmRequired: + required := true + ctx.SetValue(model.ContextKeyConfirmRequired, &required) + action = actionPartialAccepted + res = ssh.AuthPartiallySuccessful + default: + action = actionFailed } logger.Infof("%s %s for %s from %s", action, authMethod, username, remoteAddr) - return res + return + } func CheckUserPassword(ctx ssh.Context, password string) ssh.AuthResult { @@ -72,37 +81,70 @@ func CheckUserPublicKey(ctx ssh.Context, key ssh.PublicKey) ssh.AuthResult { } func CheckMFA(ctx ssh.Context, challenger gossh.KeyboardInteractiveChallenge) (res ssh.AuthResult) { + if value, ok := ctx.Value(model.ContextKeyConfirmFailed).(*bool); ok && *value { + return ssh.AuthFailed + } + username := ctx.User() remoteAddr, _, _ := net.SplitHostPort(ctx.RemoteAddr().String()) res = ssh.AuthFailed - defer func() { - authMethod := "MFA" - if res == ssh.AuthSuccessful { - action := actionAccepted - logger.Infof("%s %s for %s from %s", action, authMethod, username, remoteAddr) - } else { - action := actionFailed - logger.Errorf("%s %s for %s from %s", action, authMethod, username, remoteAddr) - } - }() - answers, err := challenger(username, mfaInstruction, []string{mfaQuestion}, []bool{true}) - if err != nil || len(answers) != 1 { + + var confirmAction bool + instruction := mfaInstruction + question := mfaQuestion + + client, ok := ctx.Value(model.ContextKeyClient).(*service.SessionClient) + if !ok { + logger.Errorf("User %s Mfa Auth failed: not found session client.", username, ) return } - mfaCode := answers[0] - seed, ok := ctx.Value(cctx.ContextKeySeed).(string) - if !ok { - logger.Error("Mfa Auth failed, may be user password or publickey auth failed") + value, ok := ctx.Value(model.ContextKeyConfirmRequired).(*bool) + if ok && *value { + confirmAction = true + instruction = confirmInstruction + question = confirmQuestion + } + answers, err := challenger(username, instruction, []string{question}, []bool{true}) + if err != nil || len(answers) != 1 { + if confirmAction { + client.CancelConfirm() + } + logger.Errorf("User %s happened err: %s", username, err) return } - resp, err := service.CheckUserOTP(seed, mfaCode, remoteAddr, "T") - if err != nil { - logger.Error("Mfa Auth failed: ", err) + if confirmAction { + switch strings.TrimSpace(strings.ToLower(answers[0])) { + case "yes", "y", "": + user, authStatus := client.CheckConfirm(ctx) + switch authStatus { + case service.AuthSuccess: + res = ssh.AuthSuccessful + ctx.SetValue(model.ContextKeyUser, &user) + return + } + case "no", "n": + client.CancelConfirm() + default: + return + } + failed := true + ctx.SetValue(model.ContextKeyConfirmFailed, &failed) return } - if resp.Token != "" { + mfaCode := answers[0] + user, authStatus := client.CheckUserOTP(ctx, mfaCode) + switch authStatus { + case service.AuthSuccess: res = ssh.AuthSuccessful - return + ctx.SetValue(model.ContextKeyUser, &user) + logger.Infof("%s MFA for %s from %s", actionAccepted, username, remoteAddr) + case service.AuthConfirmRequired: + res = ssh.AuthPartiallySuccessful + required := true + ctx.SetValue(model.ContextKeyConfirmRequired, &required) + logger.Infof("%s MFA for %s from %s", actionPartialAccepted, username, remoteAddr) + default: + logger.Errorf("%s MFA for %s from %s", actionFailed, username, remoteAddr) } return } diff --git a/jumpserver/koko/pkg/cctx/context.go b/jumpserver/koko/pkg/cctx/context.go deleted file mode 100644 index 3fc31e079a4fbbbbf5a8c44b14510245b516677b..0000000000000000000000000000000000000000 --- a/jumpserver/koko/pkg/cctx/context.go +++ /dev/null @@ -1,78 +0,0 @@ -package cctx - -import ( - "context" - - "github.com/gliderlabs/ssh" - - "github.com/jumpserver/koko/pkg/model" -) - -type contextKey struct { - name string -} - -var ( - ContextKeyUser = &contextKey{"user"} - ContextKeyAsset = &contextKey{"asset"} - ContextKeySystemUser = &contextKey{"systemUser"} - ContextKeySSHSession = &contextKey{"sshSession"} - ContextKeyLocalAddr = &contextKey{"localAddr"} - ContextKeyRemoteAddr = &contextKey{"RemoteAddr"} - ContextKeySSHCtx = &contextKey{"sshCtx"} - ContextKeySeed = &contextKey{"seed"} - ContextKeyToken = &contextKey{"token"} -) - -type Context interface { - context.Context - User() *model.User - Asset() *model.Asset - SystemUser() *model.SystemUser - SSHSession() *ssh.Session - SSHCtx() *ssh.Context - SetValue(key, value interface{}) -} - -// Context coco内部使用的Context -type CocoContext struct { - context.Context -} - -// user 返回当前连接的用户model -func (ctx *CocoContext) User() *model.User { - return ctx.Value(ContextKeyUser).(*model.User) -} - -func (ctx *CocoContext) Asset() *model.Asset { - return ctx.Value(ContextKeyAsset).(*model.Asset) -} - -func (ctx *CocoContext) SystemUser() *model.SystemUser { - return ctx.Value(ContextKeySystemUser).(*model.SystemUser) -} - -func (ctx *CocoContext) SSHSession() *ssh.Session { - return ctx.Value(ContextKeySSHSession).(*ssh.Session) -} - -func (ctx *CocoContext) SSHCtx() *ssh.Context { - return ctx.Value(ContextKeySSHCtx).(*ssh.Context) -} - -func (ctx *CocoContext) SetValue(key, value interface{}) { - ctx.Context = context.WithValue(ctx.Context, key, value) -} - -func applySessionMetadata(ctx *CocoContext, sess ssh.Session) { - ctx.SetValue(ContextKeySSHSession, &sess) - ctx.SetValue(ContextKeySSHCtx, sess.Context()) - ctx.SetValue(ContextKeyLocalAddr, sess.LocalAddr()) -} - -func NewContext(sess ssh.Session) (*CocoContext, context.CancelFunc) { - sshCtx, cancel := context.WithCancel(sess.Context()) - ctx := &CocoContext{sshCtx} - applySessionMetadata(ctx, sess) - return ctx, cancel -} diff --git a/jumpserver/koko/pkg/common/client.go b/jumpserver/koko/pkg/common/client.go index bbcd853e9b5a815801039e683bff3fcd75a8536a..70c5352b557aa0641d29c9f0e0857564595cdb20 100644 --- a/jumpserver/koko/pkg/common/client.go +++ b/jumpserver/koko/pkg/common/client.go @@ -9,6 +9,7 @@ import ( "io/ioutil" "mime/multipart" "net/http" + "net/http/cookiejar" neturl "net/url" "os" "path/filepath" @@ -38,8 +39,10 @@ type UrlParser interface { func NewClient(timeout time.Duration, baseHost string) Client { headers := make(map[string]string) + jar, _ := cookiejar.New(nil) client := http.Client{ Timeout: timeout * time.Second, + Jar: jar, } return Client{ BaseHost: baseHost, @@ -80,15 +83,16 @@ func (c *Client) parseUrlQuery(url string, params []map[string]string) string { if len(params) < 1 { return url } - var query []string - for k, v := range params[0] { - query = append(query, fmt.Sprintf("%s=%s", k, v)) + query := neturl.Values{} + for _, item := range params { + for k, v := range item { + query.Add(k, v) + } } - param := strings.Join(query, "&") if strings.Contains(url, "?") { - url += "&" + param + url += "&" + query.Encode() } else { - url += "?" + param + url += "?" + query.Encode() } return url } @@ -103,11 +107,10 @@ func (c *Client) parseUrl(url string, params []map[string]string) string { func (c *Client) setAuthHeader(r *http.Request) { if len(c.cookie) != 0 { - cookie := make([]string, 0) for k, v := range c.cookie { - cookie = append(cookie, fmt.Sprintf("%s=%s", k, v)) + c := http.Cookie{Name: k, Value: v,} + r.AddCookie(&c) } - r.Header.Add("Cookie", strings.Join(cookie, ";")) } if len(c.basicAuth) == 2 { r.SetBasicAuth(c.basicAuth[0], c.basicAuth[1]) @@ -157,15 +160,17 @@ func (c *Client) NewRequest(method, url string, body interface{}, params []map[s // 1. query string if set {"name": "ibuler"} func (c *Client) Do(method, url string, data, res interface{}, params ...map[string]string) (resp *http.Response, err error) { req, err := c.NewRequest(method, url, data, params) + if err != nil { + return + } resp, err = c.http.Do(req) if err != nil { return } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) if resp.StatusCode >= 400 { - msg := fmt.Sprintf("%s %s failed, get code: %d, %s", req.Method, req.URL, resp.StatusCode, string(body)) + msg := fmt.Sprintf("%s %s failed, get code: %d, %s", req.Method, req.URL, resp.StatusCode, body) err = errors.New(msg) return } @@ -176,12 +181,15 @@ func (c *Client) Do(method, url string, data, res interface{}, params ...map[str return } // Unmarshal response body to result struct - if res != nil && resp.StatusCode >= 200 && resp.StatusCode <= 300 { - err = json.Unmarshal(body, res) - if err != nil { - msg := fmt.Sprintf("%s %s failed, unmarshal '%s' response failed: %s", req.Method, req.URL, body[:12], err) - err = errors.New(msg) - return + if res != nil { + switch { + case strings.Contains(resp.Header.Get("Content-Type"), "application/json"): + err = json.Unmarshal(body, res) + if err != nil { + msg := fmt.Sprintf("%s %s failed, unmarshal '%s' response failed: %s", req.Method, req.URL, body[:12], err) + err = errors.New(msg) + return + } } } return diff --git a/jumpserver/koko/pkg/handler/assetpaginator.go b/jumpserver/koko/pkg/handler/assetpaginator.go new file mode 100644 index 0000000000000000000000000000000000000000..c89fa9b92388c8aaefedf9252a02dc3074b6b04f --- /dev/null +++ b/jumpserver/koko/pkg/handler/assetpaginator.go @@ -0,0 +1,507 @@ +package handler + +import ( + "sync" + + "github.com/jumpserver/koko/pkg/model" + "github.com/jumpserver/koko/pkg/service" +) + +type Paginator interface { + HasPrev() bool + HasNext() bool + CurrentPage() int + TotalCount() int + TotalPage() int + PageSize() int + SetPageSize(size int) +} + +type AssetPaginator interface { + Paginator + RetrievePageData(pageIndex int) model.AssetList + SearchAsset(key string) model.AssetList + SearchAgain(key string) model.AssetList + Name() string + SearchKeys() []string +} + +func NewRemoteAssetPaginator(user model.User, pageSize int) AssetPaginator { + p := remoteAssetsPaginator{ + user: user, + pageSize: pageSize, + currentOffset: 0, + currentPage: 1, + search: make([]string, 0, 4), + lock: new(sync.RWMutex), + } + return &p +} + +func NewLocalAssetPaginator(data model.AssetList, pageSize int) AssetPaginator { + p := localAssetsPaginator{ + allData: data, + currentData: data, + pageSize: pageSize, + currentOffset: 0, + currentPage: 1, + search: make([]string, 0, 4), + lock: new(sync.RWMutex), + } + return &p +} + +func NewNodeAssetPaginator(user model.User, node model.Node, pageSize int) AssetPaginator { + p := nodeAssetsPaginator{ + user: user, + node: node, + pageSize: pageSize, + currentOffset: 0, + currentPage: 1, + lock: new(sync.RWMutex), + } + return &p +} + +type remoteAssetsPaginator struct { + user model.User + pageSize int + + lock *sync.RWMutex + currentOffset int + search []string + + currentData model.AssetList + totalPage int + currentPage int + totalCount int + + preUrl string + nextUrl string +} + +func (r *remoteAssetsPaginator) HasPrev() bool { + r.lock.RLock() + defer r.lock.RUnlock() + return r.preUrl != "" +} + +func (r *remoteAssetsPaginator) HasNext() bool { + r.lock.RLock() + defer r.lock.RUnlock() + return r.nextUrl != "" +} + +func (r *remoteAssetsPaginator) CurrentPage() int { + r.lock.RLock() + defer r.lock.RUnlock() + return r.currentPage +} + +func (r *remoteAssetsPaginator) TotalCount() int { + r.lock.RLock() + defer r.lock.RUnlock() + return r.totalCount +} + +func (r *remoteAssetsPaginator) TotalPage() int { + r.lock.RLock() + defer r.lock.RUnlock() + return r.totalPage +} + +func (r *remoteAssetsPaginator) PageSize() int { + r.lock.RLock() + defer r.lock.RUnlock() + if r.pageSize == 0 { + // size 0, 则获取全部资产 + return r.totalCount + } + return r.pageSize +} + +func (r *remoteAssetsPaginator) SetPageSize(size int) { + r.lock.Lock() + defer r.lock.Unlock() + if size < 0 { + // size 0, 则获取全部资产 + size = 0 + } + if r.pageSize == size { + return + } + r.pageSize = size +} + +func (r *remoteAssetsPaginator) RetrievePageData(pageIndex int) model.AssetList { + r.lock.Lock() + defer r.lock.Unlock() + return r.retrievePageDta(pageIndex) +} + +func (r *remoteAssetsPaginator) SearchAsset(key string) model.AssetList { + r.lock.Lock() + defer r.lock.Unlock() + r.search = r.search[:0] + r.search = append(r.search, key) + r.currentPage = 1 + r.currentOffset = 0 + return r.retrievePageDta(1) +} + +func (r *remoteAssetsPaginator) SearchAgain(key string) model.AssetList { + r.lock.Lock() + defer r.lock.Unlock() + r.search = append(r.search, key) + r.currentPage = 1 + r.currentOffset = 0 + return r.retrievePageDta(1) +} + +func (r *remoteAssetsPaginator) Name() string { + return "remote" +} + +func (r *remoteAssetsPaginator) SearchKeys() []string { + return r.search +} + +func (r *remoteAssetsPaginator) retrievePageDta(pageIndex int) model.AssetList { + offsetPage := pageIndex - r.currentPage + totalOffset := offsetPage * r.pageSize + + r.currentOffset += totalOffset + + if r.pageSize == 0 || r.currentOffset < 0 || r.pageSize >= r.totalCount { + r.currentOffset = 0 + } + res := service.GetUserAssets(r.user.ID, r.pageSize, r.currentOffset, r.search...) + + // update page info data, + r.totalCount = res.Total + r.nextUrl = res.NextURL + r.preUrl = res.PreviousURL + r.currentData = res.Data + r.updatePageInfo() + return res.Data +} + +func (r *remoteAssetsPaginator) updatePageInfo() { + switch r.pageSize { + case 0: + r.totalPage = 1 + r.currentPage = 1 + default: + pageSize := r.pageSize + totalCount := r.totalCount + + switch totalCount % pageSize { + case 0: + r.totalPage = totalCount / pageSize + default: + r.totalPage = (totalCount / pageSize) + 1 + } + currentOffset := r.currentOffset + len(r.currentData) + + switch currentOffset % pageSize { + case 0: + r.currentPage = currentOffset / pageSize + default: + r.currentPage = (currentOffset / pageSize) + 1 + } + } +} + +type localAssetsPaginator struct { + allData model.AssetList + + currentData model.AssetList + + currentPage int + + pageSize int + totalPage int + + currentOffset int + + search []string + lock *sync.RWMutex + + currentResult model.AssetList +} + +func (l *localAssetsPaginator) Name() string { + return "local" +} + +func (l *localAssetsPaginator) SearchKeys() []string { + return l.search +} + +func (l *localAssetsPaginator) HasPrev() bool { + l.lock.RLock() + defer l.lock.RUnlock() + return l.currentPage > 1 +} + +func (l *localAssetsPaginator) HasNext() bool { + l.lock.RLock() + defer l.lock.RUnlock() + return l.currentPage < l.totalPage +} + +func (l *localAssetsPaginator) CurrentPage() int { + l.lock.RLock() + defer l.lock.RUnlock() + return l.currentPage +} + +func (l *localAssetsPaginator) TotalCount() int { + l.lock.RLock() + defer l.lock.RUnlock() + return len(l.currentData) +} + +func (l *localAssetsPaginator) TotalPage() int { + l.lock.RLock() + defer l.lock.RUnlock() + return l.totalPage +} + +func (l *localAssetsPaginator) PageSize() int { + l.lock.RLock() + defer l.lock.RUnlock() + return l.pageSize +} + +func (l *localAssetsPaginator) SetPageSize(size int) { + if size <= 0 { + size = len(l.currentData) + } + l.lock.Lock() + defer l.lock.Unlock() + + if l.pageSize == size { + return + } + l.pageSize = size +} + +func (l *localAssetsPaginator) RetrievePageData(pageIndex int) model.AssetList { + l.lock.Lock() + defer l.lock.Unlock() + return l.retrievePageData(pageIndex) +} + +func (l *localAssetsPaginator) SearchAsset(key string) model.AssetList { + l.lock.Lock() + defer l.lock.Unlock() + l.search = l.search[:0] + l.search = append(l.search, key) + l.currentData = searchFromLocalAssets(l.allData, key) + l.currentPage = 1 + l.currentOffset = 0 + return l.retrievePageData(1) +} + +func (l *localAssetsPaginator) SearchAgain(key string) model.AssetList { + l.lock.Lock() + defer l.lock.Unlock() + l.currentData = searchFromLocalAssets(l.currentData, key) + l.search = append(l.search, key) + l.currentPage = 1 + l.currentOffset = 0 + return l.retrievePageData(1) +} + +func (l *localAssetsPaginator) retrievePageData(pageIndex int) model.AssetList { + offsetPage := pageIndex - l.currentPage + totalOffset := offsetPage * l.pageSize + l.currentOffset += totalOffset + + switch { + case l.currentOffset <= 0: + l.currentOffset = 0 + case l.currentOffset >= len(l.currentData): + l.currentOffset = len(l.currentData) + case l.pageSize >= len(l.currentData): + l.currentOffset = 0 + } + + end := l.currentOffset + l.pageSize + if end >= len(l.currentData) { + end = len(l.currentData) + } + l.currentResult = l.currentData[l.currentOffset:end] + l.updatePageInfo() + return l.currentResult +} + +func (l *localAssetsPaginator) updatePageInfo() { + pageSize := l.pageSize + totalCount := len(l.currentData) + + switch totalCount % pageSize { + case 0: + l.totalPage = totalCount / pageSize + default: + l.totalPage = (totalCount / pageSize) + 1 + } + offset := l.currentOffset + len(l.currentResult) + switch offset % pageSize { + case 0: + l.currentPage = offset / pageSize + default: + l.currentPage = (offset / pageSize) + 1 + } +} + +type nodeAssetsPaginator struct { + user model.User + node model.Node + + currentPage int + pageSize int + totalPage int + totalCount int + search []string + + lock *sync.RWMutex + + currentData model.AssetList + + preUrl string + nextUrl string + currentOffset int +} + +func (n *nodeAssetsPaginator) Name() string { + return n.node.Name +} + +func (n *nodeAssetsPaginator) SearchKeys() []string { + return n.search +} + +func (n *nodeAssetsPaginator) HasPrev() bool { + n.lock.RLock() + defer n.lock.RUnlock() + return n.preUrl != "" +} + +func (n *nodeAssetsPaginator) HasNext() bool { + n.lock.RLock() + defer n.lock.RUnlock() + return n.nextUrl != "" +} + +func (n *nodeAssetsPaginator) CurrentPage() int { + n.lock.RLock() + defer n.lock.RUnlock() + return n.currentPage +} + +func (n *nodeAssetsPaginator) TotalCount() int { + n.lock.RLock() + defer n.lock.RUnlock() + return n.totalCount +} + +func (n *nodeAssetsPaginator) TotalPage() int { + n.lock.RLock() + defer n.lock.RUnlock() + return n.totalPage +} + +func (n *nodeAssetsPaginator) PageSize() int { + n.lock.RLock() + defer n.lock.RUnlock() + if n.pageSize == 0 { + // size 0, 则获取全部资产 + return n.totalCount + } + return n.pageSize +} + +func (n *nodeAssetsPaginator) SetPageSize(size int) { + n.lock.Lock() + defer n.lock.Unlock() + if size < 0 { + // size 0, 则获取全部资产 + size = 0 + } + if n.pageSize == size { + return + } + n.pageSize = size +} + +func (n *nodeAssetsPaginator) RetrievePageData(pageIndex int) model.AssetList { + n.lock.Lock() + defer n.lock.Unlock() + return n.retrievePageData(pageIndex) +} + +func (n *nodeAssetsPaginator) SearchAsset(key string) model.AssetList { + n.lock.Lock() + defer n.lock.Unlock() + n.search = n.search[:0] + n.search = append(n.search, key) + n.currentPage = 1 + n.currentOffset = 0 + return n.RetrievePageData(1) +} + +func (n *nodeAssetsPaginator) SearchAgain(key string) model.AssetList { + n.lock.Lock() + defer n.lock.Unlock() + n.search = append(n.search, key) + n.currentPage = 1 + n.currentOffset = 0 + return n.retrievePageData(1) +} + +func (n *nodeAssetsPaginator) retrievePageData(pageIndex int) model.AssetList { + offsetPage := pageIndex - n.currentPage + totalOffset := offsetPage * n.pageSize + + n.currentOffset += totalOffset + + if n.pageSize == 0 || n.currentOffset < 0 || n.pageSize >= n.totalCount { + n.currentOffset = 0 + } + res := service.GetUserNodePaginationAssets(n.user.ID, n.node.ID, + n.pageSize, n.currentOffset, n.search...) + + n.totalCount = res.Total + n.nextUrl = res.NextURL + n.preUrl = res.PreviousURL + n.currentData = res.Data + n.updatePageInfo() + return res.Data +} + +func (n *nodeAssetsPaginator) updatePageInfo() { + switch n.pageSize { + case 0: + n.totalPage = 1 + n.currentPage = 1 + default: + pageSize := n.pageSize + totalCount := n.totalCount + + switch totalCount % pageSize { + case 0: + n.totalPage = totalCount / pageSize + default: + n.totalPage = (totalCount / pageSize) + 1 + } + currentOffset := n.currentOffset + len(n.currentData) + switch currentOffset % pageSize { + case 0: + n.currentPage = currentOffset / pageSize + default: + n.currentPage = (currentOffset / pageSize) + 1 + } + } +} diff --git a/jumpserver/koko/pkg/handler/banner.go b/jumpserver/koko/pkg/handler/banner.go index 14fc9d370cbd9c5aa7ce6f11cbbf95687cb08498..c2175d2d17ea6c03fedc6b5ecf02d8cfed5a784a 100644 --- a/jumpserver/koko/pkg/handler/banner.go +++ b/jumpserver/koko/pkg/handler/banner.go @@ -45,11 +45,11 @@ type Menu []MenuItem func Initial() { defaultTitle = utils.WrapperTitle(i18n.T("Welcome to use Jumpserver open source fortress system")) menu = Menu{ - {id: 1, instruct: "ID", helpText: i18n.T("directly login")}, - {id: 2, instruct: i18n.T("part IP, Hostname, Comment"), helpText: i18n.T("to search login if unique")}, - {id: 3, instruct: i18n.T("/ + IP, Hostname, Comment"), helpText: i18n.T("to search, such as: /192.168")}, - {id: 4, instruct: "p", helpText: i18n.T("display the host you have permission")}, - {id: 5, instruct: "g", helpText: i18n.T("display the node that you have permission")}, + {id: 1, instruct: i18n.T("part IP, Hostname, Comment"), helpText: i18n.T("to search login if unique")}, + {id: 2, instruct: i18n.T("/ + IP, Hostname, Comment"), helpText: i18n.T("to search, such as: /192.168")}, + {id: 3, instruct: "p", helpText: i18n.T("display the host you have permission")}, + {id: 4, instruct: "g", helpText: i18n.T("display the node that you have permission")}, + {id: 5, instruct: "d", helpText: i18n.T("display the databases that you have permission")}, {id: 6, instruct: "r", helpText: i18n.T("refresh your assets and nodes")}, {id: 7, instruct: "h", helpText: i18n.T("print help")}, {id: 8, instruct: "q", helpText: i18n.T("exit")}, @@ -93,8 +93,8 @@ func getI18nFromMap(name string) string { "Comment": i18n.T("comment"), "AssetTableCaption": i18n.T("Page: %d, Count: %d, Total Page: %d, Total Count: %d"), "NoAssets": i18n.T("No Assets"), - "LoginTip": i18n.T("\nTips: Enter the asset ID and directly login the asset.\n"), - "PageActionTip": i18n.T("\nPage up: P/p Page down: Enter|N/n BACK: b.\n"), + "LoginTip": i18n.T("Enter ID number directly login the asset, multiple search use // + field, such as: //16"), + "PageActionTip": i18n.T("Page up: b Page down: n"), "NodeHeaderTip": i18n.T("Node: [ ID.Name(Asset amount) ]"), "NodeEndTip": i18n.T("Tips: Enter g+NodeID to display the host under the node, such as g1"), "RefreshDone": i18n.T("Refresh done"), @@ -102,6 +102,12 @@ func getI18nFromMap(name string) string { "BackTip": i18n.T("Back: B/b"), "Name": i18n.T("Name"), "Username": i18n.T("Username"), + "All": i18n.T("all"), + "SearchTip": i18n.T("Search: %s"), + "DBType": i18n.T("DBType"), + "DBName": i18n.T("DB Name"), + "NoDatabases": i18n.T("No Databases"), + "DBLoginTip": i18n.T("Enter ID number directly login the database, multiple search use // + field, such as: //16"), } }) return i18nMap[name] diff --git a/jumpserver/koko/pkg/handler/dbpaginator.go b/jumpserver/koko/pkg/handler/dbpaginator.go new file mode 100644 index 0000000000000000000000000000000000000000..2f38179078d0a9ce46c1a752bf2b6fccf96b1891 --- /dev/null +++ b/jumpserver/koko/pkg/handler/dbpaginator.go @@ -0,0 +1,187 @@ +package handler + +import ( + "strings" + "sync" + + "github.com/jumpserver/koko/pkg/model" +) + +type DatabasePaginator interface { + Paginator + RetrievePageData(pageIndex int) []model.Database + SearchAsset(key string) []model.Database + SearchAgain(key string) []model.Database + Name() string + SearchKeys() []string +} + +func NewLocalDatabasePaginator(data [] model.Database, pageSize int) DatabasePaginator { + p := localDatabasePaginator{ + allData: data, + currentData: data, + pageSize: pageSize, + currentOffset: 0, + currentPage: 1, + search: make([]string, 0, 4), + lock: new(sync.RWMutex), + } + return &p +} + +type localDatabasePaginator struct { + allData []model.Database + + currentData []model.Database + + currentPage int + + pageSize int + totalPage int + + currentOffset int + + search []string + lock *sync.RWMutex + + currentResult []model.Database +} + +func (l *localDatabasePaginator) Name() string { + return "local" +} + +func (l *localDatabasePaginator) SearchKeys() []string { + return l.search +} + +func (l *localDatabasePaginator) HasPrev() bool { + l.lock.RLock() + defer l.lock.RUnlock() + return l.currentPage > 1 +} + +func (l *localDatabasePaginator) HasNext() bool { + l.lock.RLock() + defer l.lock.RUnlock() + return l.currentPage < l.totalPage +} + +func (l *localDatabasePaginator) CurrentPage() int { + l.lock.RLock() + defer l.lock.RUnlock() + return l.currentPage +} + +func (l *localDatabasePaginator) TotalCount() int { + l.lock.RLock() + defer l.lock.RUnlock() + return len(l.currentData) +} + +func (l *localDatabasePaginator) TotalPage() int { + l.lock.RLock() + defer l.lock.RUnlock() + return l.totalPage +} + +func (l *localDatabasePaginator) PageSize() int { + l.lock.RLock() + defer l.lock.RUnlock() + return l.pageSize +} + +func (l *localDatabasePaginator) SetPageSize(size int) { + if size <= 0 { + size = len(l.currentData) + } + l.lock.Lock() + defer l.lock.Unlock() + + if l.pageSize == size { + return + } + l.pageSize = size +} + +func (l *localDatabasePaginator) RetrievePageData(pageIndex int) []model.Database { + l.lock.Lock() + defer l.lock.Unlock() + return l.retrievePageData(pageIndex) +} + +func (l *localDatabasePaginator) SearchAsset(key string) []model.Database { + l.lock.Lock() + defer l.lock.Unlock() + l.search = l.search[:0] + l.search = append(l.search, key) + l.currentData = searchFromLocalDBs(l.allData, key) + l.currentPage = 1 + l.currentOffset = 0 + return l.retrievePageData(1) +} + +func (l *localDatabasePaginator) SearchAgain(key string) []model.Database { + l.lock.Lock() + defer l.lock.Unlock() + l.currentData = searchFromLocalDBs(l.currentData, key) + l.search = append(l.search, key) + l.currentPage = 1 + l.currentOffset = 0 + return l.retrievePageData(1) +} + +func (l *localDatabasePaginator) retrievePageData(pageIndex int) []model.Database { + offsetPage := pageIndex - l.currentPage + totalOffset := offsetPage * l.pageSize + l.currentOffset += totalOffset + + switch { + case l.currentOffset <= 0: + l.currentOffset = 0 + case l.currentOffset >= len(l.currentData): + l.currentOffset = len(l.currentData) + case l.pageSize >= len(l.currentData): + l.currentOffset = 0 + } + + end := l.currentOffset + l.pageSize + if end >= len(l.currentData) { + end = len(l.currentData) + } + l.currentResult = l.currentData[l.currentOffset:end] + l.updatePageInfo() + return l.currentResult +} + +func (l *localDatabasePaginator) updatePageInfo() { + pageSize := l.pageSize + totalCount := len(l.currentData) + + switch totalCount % pageSize { + case 0: + l.totalPage = totalCount / pageSize + default: + l.totalPage = (totalCount / pageSize) + 1 + } + offset := l.currentOffset + len(l.currentResult) + switch offset % pageSize { + case 0: + l.currentPage = offset / pageSize + default: + l.currentPage = (offset / pageSize) + 1 + } +} + +func searchFromLocalDBs(dbs []model.Database, key string) []model.Database { + displayDBs := make([]model.Database, 0, len(dbs)) + key = strings.ToLower(key) + for _, db := range dbs { + contents := []string{strings.ToLower(db.Name),strings.ToLower(db.DBName), + strings.ToLower(db.Host), strings.ToLower(db.Comment)} + if isSubstring(contents, key) { + displayDBs = append(displayDBs, db) + } + } + return displayDBs +} diff --git a/jumpserver/koko/pkg/handler/dispatch.go b/jumpserver/koko/pkg/handler/dispatch.go new file mode 100644 index 0000000000000000000000000000000000000000..79491be8ee6a506ae2108240dcd21f4436f977dd --- /dev/null +++ b/jumpserver/koko/pkg/handler/dispatch.go @@ -0,0 +1,517 @@ +package handler + +import ( + "fmt" + "io" + "strconv" + "strings" + + "github.com/jumpserver/koko/pkg/common" + "github.com/jumpserver/koko/pkg/config" + "github.com/jumpserver/koko/pkg/logger" + "github.com/jumpserver/koko/pkg/model" + "github.com/jumpserver/koko/pkg/proxy" + "github.com/jumpserver/koko/pkg/service" + "github.com/jumpserver/koko/pkg/utils" +) + +func (h *interactiveHandler) Dispatch() { + defer logger.Infof("Request %s: User %s stop interactive", h.sess.ID(), h.user.Name) + for { + line, err := h.term.ReadLine() + if err != nil { + logger.Debugf("User %s close connect", h.user.Name) + break + } + line = strings.TrimSpace(line) + switch len(line) { + case 0, 1: + switch strings.ToLower(line) { + case "p": + h.resetPaginator() + case "b": + if h.assetPaginator != nil { + h.movePrePage() + break + } + if h.dbPaginator != nil { + h.moveDBPrePage() + break + } + if ok := h.searchOrProxy(line); ok { + continue + } + case "d": + h.assetPaginator = nil + h.dbPaginator = h.getDatabasePaginator() + h.currentDBData = h.dbPaginator.RetrievePageData(1) + case "n": + if h.assetPaginator != nil { + h.moveNextPage() + break + } + if h.dbPaginator != nil { + h.moveDBNextPage() + break + } + if ok := h.searchOrProxy(line); ok { + continue + } + case "": + if h.assetPaginator != nil { + h.moveNextPage() + } else if h.dbPaginator != nil { + h.moveDBNextPage() + } else { + h.resetPaginator() + } + case "g": + h.displayNodeTree() + continue + case "h": + h.displayBanner() + continue + case "r": + h.refreshAssetsAndNodesData() + continue + case "q": + logger.Debugf("user %s enter to exit", h.user.Name) + return + default: + if ok := h.searchOrProxy(line); ok { + continue + } + } + default: + switch { + case line == "exit", line == "quit": + logger.Debugf("user %s enter to exit", h.user.Name) + return + case strings.Index(line, "/") == 0: + if strings.Index(line[1:], "/") == 0 { + line = strings.TrimSpace(line[2:]) + h.searchAssetsAgain(line) + break + } + line = strings.TrimSpace(line[1:]) + h.searchAssetAndDisplay(line) + case strings.Index(line, "g") == 0: + searchWord := strings.TrimSpace(strings.TrimPrefix(line, "g")) + if num, err := strconv.Atoi(searchWord); err == nil { + if num >= 0 { + h.searchNewNodeAssets(num) + break + } + } + if ok := h.searchOrProxy(line); ok { + continue + } + default: + if ok := h.searchOrProxy(line); ok { + continue + } + } + } + if h.dbPaginator != nil { + h.displayPageDatabase() + } + if h.assetPaginator != nil { + h.displayPageAssets() + } + + } +} + +func (h *interactiveHandler) resetPaginator() { + h.dbPaginator = nil + h.currentDBData = nil + h.assetPaginator = h.getAssetPaginator() + h.currentData = h.assetPaginator.RetrievePageData(1) +} + +func (h *interactiveHandler) displayPageAssets() { + if len(h.currentData) == 0 { + _, _ = h.term.Write([]byte(getI18nFromMap("NoAssets") + "\n\r")) + h.assetPaginator = nil + h.currentSortedData = nil + return + } + Labels := []string{getI18nFromMap("ID"), getI18nFromMap("Hostname"), + getI18nFromMap("IP"), getI18nFromMap("Comment")} + fields := []string{"ID", "hostname", "IP", "comment"} + h.currentSortedData = model.AssetList(h.currentData).SortBy(config.GetConf().AssetListSortBy) + data := make([]map[string]string, len(h.currentSortedData)) + for i, j := range h.currentSortedData { + row := make(map[string]string) + row["ID"] = strconv.Itoa(i + 1) + row["hostname"] = j.Hostname + row["IP"] = j.IP + + comments := make([]string, 0) + for _, item := range strings.Split(strings.TrimSpace(j.Comment), "\r\n") { + if strings.TrimSpace(item) == "" { + continue + } + comments = append(comments, strings.ReplaceAll(strings.TrimSpace(item), " ", ",")) + } + row["comment"] = strings.Join(comments, "|") + data[i] = row + } + w, _ := h.term.GetSize() + + currentPage := h.assetPaginator.CurrentPage() + pageSize := h.assetPaginator.PageSize() + totalPage := h.assetPaginator.TotalPage() + totalCount := h.assetPaginator.TotalCount() + + caption := fmt.Sprintf(getI18nFromMap("AssetTableCaption"), + currentPage, pageSize, totalPage, totalCount) + + caption = utils.WrapperString(caption, utils.Green) + table := common.WrapperTable{ + Fields: fields, + Labels: Labels, + FieldsSize: map[string][3]int{ + "ID": {0, 0, 5}, + "hostname": {0, 8, 0}, + "IP": {0, 15, 40}, + "comment": {0, 0, 0}, + }, + Data: data, + TotalSize: w, + Caption: caption, + TruncPolicy: common.TruncMiddle, + } + table.Initial() + header := getI18nFromMap("All") + keys := h.assetPaginator.SearchKeys() + switch h.assetPaginator.Name() { + case "local", "remote": + if len(keys) != 0 { + header = strings.Join(keys, " ") + } + default: + header = fmt.Sprintf("%s %s", h.assetPaginator.Name(), strings.Join(keys, " ")) + } + searchHeader := fmt.Sprintf(getI18nFromMap("SearchTip"), header) + actionTip := fmt.Sprintf("%s %s", getI18nFromMap("LoginTip"), getI18nFromMap("PageActionTip")) + + _, _ = h.term.Write([]byte(utils.CharClear)) + _, _ = h.term.Write([]byte(table.Display())) + utils.IgnoreErrWriteString(h.term, utils.WrapperString(actionTip, utils.Green)) + utils.IgnoreErrWriteString(h.term, utils.CharNewLine) + utils.IgnoreErrWriteString(h.term, utils.WrapperString(searchHeader, utils.Green)) + utils.IgnoreErrWriteString(h.term, utils.CharNewLine) +} + +func (h *interactiveHandler) movePrePage() { + if h.assetPaginator == nil || !h.assetPaginator.HasPrev() { + return + } + h.assetPaginator.SetPageSize(getPageSize(h.term)) + prePage := h.assetPaginator.CurrentPage() - 1 + h.currentData = h.assetPaginator.RetrievePageData(prePage) +} + +func (h *interactiveHandler) moveNextPage() { + if h.assetPaginator == nil || !h.assetPaginator.HasNext() { + return + } + h.assetPaginator.SetPageSize(getPageSize(h.term)) + nextPage := h.assetPaginator.CurrentPage() + 1 + h.currentData = h.assetPaginator.RetrievePageData(nextPage) +} + +func (h *interactiveHandler) searchAssets(key string) []model.Asset { + if _, ok := h.assetPaginator.(*nodeAssetsPaginator); ok { + h.assetPaginator = nil + } + if h.assetPaginator == nil { + h.assetPaginator = h.getAssetPaginator() + } + return h.assetPaginator.SearchAsset(key) + +} + +func (h *interactiveHandler) searchOrProxy(key string) bool { + if h.dbPaginator != nil { + if indexNum, err := strconv.Atoi(key); err == nil && len(h.currentDBData) > 0 { + if indexNum > 0 && indexNum <= len(h.currentDBData) { + dbSelected := h.currentDBData[indexNum-1] + h.ProxyDB(dbSelected) + h.dbPaginator = nil + h.currentDBData = nil + return true + } + } + if data := h.dbPaginator.SearchAsset(key); len(data) == 1 { + h.ProxyDB(data[0]) + h.dbPaginator = nil + h.currentDBData = nil + return true + } else { + h.currentDBData = data + } + return false + } + if indexNum, err := strconv.Atoi(key); err == nil && len(h.currentSortedData) > 0 { + if indexNum > 0 && indexNum <= len(h.currentSortedData) { + assetSelect := h.currentSortedData[indexNum-1] + h.ProxyAsset(assetSelect) + h.assetPaginator = nil + h.currentSortedData = nil + return true + } + } + if data := h.searchAssets(key); len(data) == 1 { + h.ProxyAsset(data[0]) + h.assetPaginator = nil + h.currentSortedData = nil + return true + } else { + h.currentData = data + } + return false +} + +func (h *interactiveHandler) searchAssetAndDisplay(key string) { + h.currentDBData = nil + h.dbPaginator = nil + h.currentData = h.searchAssets(key) +} + +func (h *interactiveHandler) searchAssetsAgain(key string) { + if h.dbPaginator != nil { + h.currentDBData = h.dbPaginator.SearchAgain(key) + return + } + if h.assetPaginator == nil { + h.assetPaginator = h.getAssetPaginator() + h.currentData = h.assetPaginator.SearchAsset(key) + return + } + h.currentData = h.assetPaginator.SearchAgain(key) +} + +func (h *interactiveHandler) displayNodeTree() { + <-h.firstLoadDone + tree := ConstructAssetNodeTree(h.nodes) + _, _ = io.WriteString(h.term, "\n\r"+getI18nFromMap("NodeHeaderTip")) + _, _ = io.WriteString(h.term, tree.String()) + _, err := io.WriteString(h.term, getI18nFromMap("NodeEndTip")+"\n\r") + if err != nil { + logger.Info("displayAssetNodes err:", err) + } +} + +func (h *interactiveHandler) searchNewNodeAssets(num int) { + <-h.firstLoadDone + + if num > len(h.nodes) || num == 0 { + h.currentData = nil + return + } + node := h.nodes[num-1] + h.assetPaginator = h.getNodeAssetPaginator(node) + h.currentData = h.assetPaginator.RetrievePageData(1) +} + +func (h *interactiveHandler) getAssetPaginator() AssetPaginator { + switch h.assetLoadPolicy { + case "all": + <-h.firstLoadDone + return NewLocalAssetPaginator(h.allAssets, getPageSize(h.term)) + default: + } + return NewRemoteAssetPaginator(*h.user, getPageSize(h.term)) +} + +func (h *interactiveHandler) getNodeAssetPaginator(node model.Node) AssetPaginator { + return NewNodeAssetPaginator(*h.user, node, getPageSize(h.term)) +} + +func (h *interactiveHandler) getDatabasePaginator() DatabasePaginator { + dbs := service.GetUserDatabases(h.user.ID) + return NewLocalDatabasePaginator(dbs, getPageSize(h.term)) +} + +func (h *interactiveHandler) displayPageDatabase() { + if len(h.currentDBData) == 0 { + _, _ = h.term.Write([]byte(getI18nFromMap("NoDatabases") + "\n\r")) + h.dbPaginator = nil + return + } + Labels := []string{getI18nFromMap("ID"), getI18nFromMap("Name"), + getI18nFromMap("IP"), getI18nFromMap("DBType"), + getI18nFromMap("DBName"),getI18nFromMap("Comment")} + fields := []string{"ID", "name", "IP", "DBType","DBName", "comment"} + data := make([]map[string]string, len(h.currentDBData)) + for i, j := range h.currentDBData { + row := make(map[string]string) + row["ID"] = strconv.Itoa(i + 1) + row["name"] = j.Name + row["IP"] = j.Host + row["DBType"] = j.DBType + row["DBName"] = j.DBName + + comments := make([]string, 0) + for _, item := range strings.Split(strings.TrimSpace(j.Comment), "\r\n") { + if strings.TrimSpace(item) == "" { + continue + } + comments = append(comments, strings.ReplaceAll(strings.TrimSpace(item), " ", ",")) + } + row["comment"] = strings.Join(comments, "|") + data[i] = row + } + w, _ := h.term.GetSize() + + currentPage := h.dbPaginator.CurrentPage() + pageSize := h.dbPaginator.PageSize() + totalPage := h.dbPaginator.TotalPage() + totalCount := h.dbPaginator.TotalCount() + + caption := fmt.Sprintf(getI18nFromMap("AssetTableCaption"), + currentPage, pageSize, totalPage, totalCount) + + caption = utils.WrapperString(caption, utils.Green) + table := common.WrapperTable{ + Fields: fields, + Labels: Labels, + FieldsSize: map[string][3]int{ + "ID": {0, 0, 5}, + "name": {0, 8, 0}, + "IP": {0, 15, 40}, + "DBType": {0, 8, 0}, + "DBName": {0, 8, 0}, + "comment": {0, 0, 0}, + }, + Data: data, + TotalSize: w, + Caption: caption, + TruncPolicy: common.TruncMiddle, + } + table.Initial() + header := getI18nFromMap("All") + keys := h.dbPaginator.SearchKeys() + switch h.dbPaginator.Name() { + case "local", "remote": + if len(keys) != 0 { + header = strings.Join(keys, " ") + } + default: + header = fmt.Sprintf("%s %s", h.dbPaginator.Name(), strings.Join(keys, " ")) + } + searchHeader := fmt.Sprintf(getI18nFromMap("SearchTip"), header) + actionTip := fmt.Sprintf("%s %s", getI18nFromMap("DBLoginTip"), getI18nFromMap("PageActionTip")) + + _, _ = h.term.Write([]byte(utils.CharClear)) + _, _ = h.term.Write([]byte(table.Display())) + utils.IgnoreErrWriteString(h.term, utils.WrapperString(actionTip, utils.Green)) + utils.IgnoreErrWriteString(h.term, utils.CharNewLine) + utils.IgnoreErrWriteString(h.term, utils.WrapperString(searchHeader, utils.Green)) + utils.IgnoreErrWriteString(h.term, utils.CharNewLine) +} + +func (h *interactiveHandler) moveDBPrePage() { + if h.dbPaginator == nil || !h.dbPaginator.HasPrev() { + return + } + h.dbPaginator.SetPageSize(getPageSize(h.term)) + prePage := h.dbPaginator.CurrentPage() - 1 + h.currentDBData = h.dbPaginator.RetrievePageData(prePage) +} + +func (h *interactiveHandler) moveDBNextPage() { + if h.dbPaginator == nil || !h.dbPaginator.HasNext() { + return + } + h.dbPaginator.SetPageSize(getPageSize(h.term)) + prePage := h.dbPaginator.CurrentPage() + 1 + h.currentDBData = h.dbPaginator.RetrievePageData(prePage) +} + +func (h *interactiveHandler) ProxyDB(dbSelect model.Database) { + systemUsers := service.GetUserDatabaseSystemUsers(h.user.ID, dbSelect.ID) + systemUserSelect, ok := h.chooseDBSystemUser(dbSelect, systemUsers) + if !ok { + return + } + p := proxy.DBProxyServer{ + UserConn: h.sess, + User: h.user, + Database: &dbSelect, + SystemUser: &systemUserSelect, + } + h.pauseWatchWinSize() + p.Proxy() + logger.Infof("Request %s: database %s proxy end", h.sess.Uuid, dbSelect.Name) + h.resumeWatchWinSize() +} + +func (h *interactiveHandler) chooseDBSystemUser(dbAsset model.Database, + systemUsers []model.SystemUser) (systemUser model.SystemUser, ok bool) { + + length := len(systemUsers) + switch length { + case 0: + return model.SystemUser{}, false + case 1: + return systemUsers[0], true + default: + } + displaySystemUsers := selectHighestPrioritySystemUsers(systemUsers) + if len(displaySystemUsers) == 1 { + return displaySystemUsers[0], true + } + + Labels := []string{getI18nFromMap("ID"), getI18nFromMap("Name"), getI18nFromMap("Username")} + fields := []string{"ID", "Name", "Username"} + + data := make([]map[string]string, len(displaySystemUsers)) + for i, j := range displaySystemUsers { + row := make(map[string]string) + row["ID"] = strconv.Itoa(i + 1) + row["Name"] = j.Name + row["Username"] = j.Username + data[i] = row + } + w, _ := h.term.GetSize() + table := common.WrapperTable{ + Fields: fields, + Labels: Labels, + FieldsSize: map[string][3]int{ + "ID": {0, 0, 5}, + "Name": {0, 8, 0}, + "Username": {0, 10, 0}, + }, + Data: data, + TotalSize: w, + TruncPolicy: common.TruncMiddle, + } + table.Initial() + + h.term.SetPrompt("ID> ") + defer h.term.SetPrompt("Opt> ") + selectUserTip := fmt.Sprintf(getI18nFromMap("SelectUserTip"), dbAsset.Name, dbAsset.Host) + for { + utils.IgnoreErrWriteString(h.term, table.Display()) + utils.IgnoreErrWriteString(h.term, selectUserTip) + utils.IgnoreErrWriteString(h.term, getI18nFromMap("BackTip")) + utils.IgnoreErrWriteString(h.term, "\r\n") + line, err := h.term.ReadLine() + if err != nil { + return + } + line = strings.TrimSpace(line) + switch strings.ToLower(line) { + case "q", "b", "quit", "exit", "back": + return + } + if num, err := strconv.Atoi(line); err == nil { + if num > 0 && num <= len(displaySystemUsers) { + return displaySystemUsers[num-1], true + } + } + } +} diff --git a/jumpserver/koko/pkg/handler/pagination.go b/jumpserver/koko/pkg/handler/pagination.go deleted file mode 100644 index b0995a25c743a3c05adebdded5e227dc67ecc49e..0000000000000000000000000000000000000000 --- a/jumpserver/koko/pkg/handler/pagination.go +++ /dev/null @@ -1,391 +0,0 @@ -package handler - -import ( - "fmt" - "io" - "strconv" - "strings" - - "github.com/jumpserver/koko/pkg/common" - "github.com/jumpserver/koko/pkg/config" - "github.com/jumpserver/koko/pkg/model" - "github.com/jumpserver/koko/pkg/service" - "github.com/jumpserver/koko/pkg/utils" -) - -func NewAssetPagination(term *utils.Terminal, assets []model.Asset) AssetPagination { - assetPage := AssetPagination{term: term, assets: assets} - assetPage.Initial() - return assetPage -} - -type AssetPagination struct { - term *utils.Terminal - assets []model.Asset - page *common.Pagination - currentData []model.Asset -} - -func (p *AssetPagination) Initial() { - pageData := make([]interface{}, len(p.assets)) - for i, v := range p.assets { - pageData[i] = v - } - pageSize := p.getPageSize() - p.page = common.NewPagination(pageData, pageSize) - firstPageData := p.page.GetPageData(1) - p.currentData = make([]model.Asset, len(firstPageData)) - for i, item := range firstPageData { - p.currentData[i] = item.(model.Asset) - } -} - -func (p *AssetPagination) getPageSize() int { - var ( - pageSize int - minHeight = 8 // 分页显示的最小高度 - ) - _, height := p.term.GetSize() - switch config.GetConf().AssetListPageSize { - case "auto": - pageSize = height - minHeight - case "all": - pageSize = len(p.assets) - default: - if value, err := strconv.Atoi(config.GetConf().AssetListPageSize); err == nil { - pageSize = value - } else { - pageSize = height - minHeight - } - } - if pageSize <= 0 { - pageSize = 1 - } - return pageSize -} - -func (p *AssetPagination) Start() []model.Asset { - p.term.SetPrompt(": ") - defer p.term.SetPrompt("Opt> ") - for { - // 总数据小于page size,则显示所有资产且退出 - if p.page.PageSize() >= p.page.TotalCount() { - p.currentData = p.assets - p.displayPageAssets() - return []model.Asset{} - } - - p.displayPageAssets() - p.displayTipsInfo() - line, err := p.term.ReadLine() - if err != nil { - return []model.Asset{} - } - pageSize := p.getPageSize() - p.page.SetPageSize(pageSize) - - line = strings.TrimSpace(line) - switch len(line) { - case 0, 1: - switch strings.ToLower(line) { - case "p": - if !p.page.HasPrev() { - continue - } - prePageData := p.page.GetPrevPageData() - if len(p.currentData) != len(prePageData) { - p.currentData = make([]model.Asset, len(prePageData)) - } - for i, item := range prePageData { - p.currentData[i] = item.(model.Asset) - } - - case "", "n": - if !p.page.HasNext() { - continue - } - nextPageData := p.page.GetNextPageData() - if len(p.currentData) != len(nextPageData) { - p.currentData = make([]model.Asset, len(nextPageData)) - } - for i, item := range nextPageData { - p.currentData[i] = item.(model.Asset) - } - case "b", "q": - return []model.Asset{} - default: - if indexID, err := strconv.Atoi(line); err == nil { - if indexID > 0 && indexID <= len(p.currentData) { - return []model.Asset{p.currentData[indexID-1]} - } - } - } - default: - if indexID, err := strconv.Atoi(line); err == nil { - if indexID > 0 && indexID <= len(p.currentData) { - return []model.Asset{p.currentData[indexID-1]} - } - } - } - } -} - -func (p *AssetPagination) displayPageAssets() { - Labels := []string{getI18nFromMap("ID"), getI18nFromMap("Hostname"), - getI18nFromMap("IP"), getI18nFromMap("Comment")} - fields := []string{"ID", "hostname", "IP", "comment"} - data := make([]map[string]string, len(p.currentData)) - for i, j := range p.currentData { - row := make(map[string]string) - row["ID"] = strconv.Itoa(i + 1) - row["hostname"] = j.Hostname - row["IP"] = j.IP - - comments := make([]string, 0) - for _, item := range strings.Split(strings.TrimSpace(j.Comment), "\r\n") { - if strings.TrimSpace(item) == "" { - continue - } - comments = append(comments, strings.ReplaceAll(strings.TrimSpace(item), " ", ",")) - } - row["comment"] = strings.Join(comments, "|") - data[i] = row - } - w, _ := p.term.GetSize() - caption := fmt.Sprintf(getI18nFromMap("AssetTableCaption"), - p.page.CurrentPage(), p.page.PageSize(), p.page.TotalPage(), p.page.TotalCount(), - ) - caption = utils.WrapperString(caption, utils.Green) - table := common.WrapperTable{ - Fields: fields, - Labels: Labels, - FieldsSize: map[string][3]int{ - "ID": {0, 0, 5}, - "hostname": {0, 8, 0}, - "IP": {0, 15, 40}, - "comment": {0, 0, 0}, - }, - Data: data, - TotalSize: w, - Caption: caption, - TruncPolicy: common.TruncMiddle, - } - table.Initial() - - _, _ = p.term.Write([]byte(utils.CharClear)) - _, _ = p.term.Write([]byte(table.Display())) -} - -func (p *AssetPagination) displayTipsInfo() { - displayAssetPaginationTipsInfo(p.term) - -} - -func NewUserPagination(term *utils.Terminal, uid, search string, policy bool) UserAssetPagination { - return UserAssetPagination{ - UserID: uid, - offset: 0, - limit: 0, - search: search, - term: term, - displayPolicy: policy, - Data: model.AssetsPaginationResponse{}, - } -} - -type UserAssetPagination struct { - UserID string - offset int - limit int - search string - term *utils.Terminal - displayPolicy bool - Data model.AssetsPaginationResponse - IsNeedProxy bool - currentData []model.Asset -} - -func (p *UserAssetPagination) Start() []model.Asset { - p.term.SetPrompt(": ") - defer p.term.SetPrompt("Opt> ") - for { - p.retrieveData() - - if p.displayPolicy && p.Data.Total == 1 { - p.IsNeedProxy = true - return p.Data.Data - } - - // 无上下页,则退出循环 - if p.Data.NextURL == "" && p.Data.PreviousURL == "" { - p.displayPageAssets() - return p.currentData - } - - inLoop: - p.displayPageAssets() - p.displayTipsInfo() - line, err := p.term.ReadLine() - if err != nil { - return p.currentData - } - - line = strings.TrimSpace(line) - switch len(line) { - case 0, 1: - switch strings.ToLower(line) { - case "p": - if p.Data.PreviousURL == "" { - continue - } - p.offset -= p.limit - case "", "n": - if p.Data.NextURL == "" { - continue - } - p.offset += p.limit - case "b", "q": - return []model.Asset{} - default: - if indexID, err := strconv.Atoi(line); err == nil { - if indexID > 0 && indexID <= len(p.currentData) { - p.IsNeedProxy = true - return []model.Asset{p.currentData[indexID-1]} - } - } - goto inLoop - } - default: - if indexID, err := strconv.Atoi(line); err == nil { - if indexID > 0 && indexID <= len(p.currentData) { - p.IsNeedProxy = true - return []model.Asset{p.currentData[indexID-1]} - } - } - goto inLoop - } - } -} - -func (p *UserAssetPagination) displayPageAssets() { - if len(p.Data.Data) == 0 { - _, _ = p.term.Write([]byte(getI18nFromMap("NoAssets") + "\n\r")) - return - } - - Labels := []string{getI18nFromMap("ID"), getI18nFromMap("Hostname"), - getI18nFromMap("IP"), getI18nFromMap("Comment")} - fields := []string{"ID", "hostname", "IP", "comment"} - p.currentData = model.AssetList(p.Data.Data).SortBy(config.GetConf().AssetListSortBy) - data := make([]map[string]string, len(p.currentData)) - for i, j := range p.currentData { - row := make(map[string]string) - row["ID"] = strconv.Itoa(i + 1) - row["hostname"] = j.Hostname - row["IP"] = j.IP - - comments := make([]string, 0) - for _, item := range strings.Split(strings.TrimSpace(j.Comment), "\r\n") { - if strings.TrimSpace(item) == "" { - continue - } - comments = append(comments, strings.ReplaceAll(strings.TrimSpace(item), " ", ",")) - } - row["comment"] = strings.Join(comments, "|") - data[i] = row - } - w, _ := p.term.GetSize() - var pageSize int - var totalPage int - var currentPage int - var totalCount int - currentOffset := p.offset + len(p.currentData) - switch p.limit { - case 0: - pageSize = len(p.currentData) - totalCount = pageSize - totalPage = 1 - currentPage = 1 - default: - pageSize = p.limit - totalCount = p.Data.Total - - switch totalCount % pageSize { - case 0: - totalPage = totalCount / pageSize - default: - totalPage = (totalCount / pageSize) + 1 - } - switch currentOffset % pageSize { - case 0: - currentPage = currentOffset / pageSize - default: - currentPage = (currentOffset / pageSize) + 1 - } - } - caption := fmt.Sprintf(getI18nFromMap("AssetTableCaption"), - currentPage, pageSize, totalPage, totalCount) - - caption = utils.WrapperString(caption, utils.Green) - table := common.WrapperTable{ - Fields: fields, - Labels: Labels, - FieldsSize: map[string][3]int{ - "ID": {0, 0, 5}, - "hostname": {0, 8, 0}, - "IP": {0, 15, 40}, - "comment": {0, 0, 0}, - }, - Data: data, - TotalSize: w, - Caption: caption, - TruncPolicy: common.TruncMiddle, - } - table.Initial() - - _, _ = p.term.Write([]byte(utils.CharClear)) - _, _ = p.term.Write([]byte(table.Display())) -} - -func (p *UserAssetPagination) displayTipsInfo() { - displayAssetPaginationTipsInfo(p.term) -} - -func (p *UserAssetPagination) retrieveData() { - p.limit = getPageSize(p.term) - if p.limit == 0 || p.offset < 0 || p.limit >= p.Data.Total { - p.offset = 0 - } - p.Data = service.GetUserAssets(p.UserID, p.search, p.limit, p.offset) -} - -func getPageSize(term *utils.Terminal) int { - var ( - pageSize int - minHeight = 8 // 分页显示的最小高度 - - ) - _, height := term.GetSize() - conf := config.GetConf() - switch conf.AssetListPageSize { - case "auto": - pageSize = height - minHeight - case "all": - return 0 - default: - if value, err := strconv.Atoi(conf.AssetListPageSize); err == nil { - pageSize = value - } else { - pageSize = height - minHeight - } - } - if pageSize <= 0 { - pageSize = 1 - } - return pageSize -} - -func displayAssetPaginationTipsInfo(w io.Writer) { - utils.IgnoreErrWriteString(w, getI18nFromMap("LoginTip")) - utils.IgnoreErrWriteString(w, getI18nFromMap("PageActionTip")) -} diff --git a/jumpserver/koko/pkg/handler/session.go b/jumpserver/koko/pkg/handler/session.go index 9b0151c3f7cc2aab8b6f9f2d3b942a2cbc752164..48fe26c6227c96ca24598b31bd5948ae13187f83 100644 --- a/jumpserver/koko/pkg/handler/session.go +++ b/jumpserver/koko/pkg/handler/session.go @@ -10,7 +10,6 @@ import ( "github.com/gliderlabs/ssh" "github.com/xlab/treeprint" - "github.com/jumpserver/koko/pkg/cctx" "github.com/jumpserver/koko/pkg/common" "github.com/jumpserver/koko/pkg/config" "github.com/jumpserver/koko/pkg/logger" @@ -21,13 +20,17 @@ import ( ) func SessionHandler(sess ssh.Session) { + user, ok := sess.Context().Value(model.ContextKeyUser).(*model.User) + if !ok || user.ID == "" { + logger.Errorf("SSH User %s not found, exit.", sess.User()) + return + } pty, _, ok := sess.Pty() if ok { - ctx, cancel := cctx.NewContext(sess) - defer cancel() - handler := newInteractiveHandler(sess, ctx.User()) + handler := newInteractiveHandler(sess, user) logger.Infof("Request %s: User %s request pty %s", handler.sess.ID(), sess.User(), pty.Term) - handler.Dispatch(ctx) + go handler.watchWinSizeChange() + handler.Dispatch() } else { utils.IgnoreErrWriteString(sess, "No PTY requested.\n") return @@ -55,19 +58,26 @@ type interactiveHandler struct { assetSelect *model.Asset systemUserSelect *model.SystemUser nodes model.NodeList - searchResult []model.Asset allAssets []model.Asset - loadDataDone chan struct{} + firstLoadDone chan struct{} assetLoadPolicy string + + currentSortedData []model.Asset + currentData []model.Asset + + assetPaginator AssetPaginator + + dbPaginator DatabasePaginator + currentDBData []model.Database } func (h *interactiveHandler) Initial() { h.assetLoadPolicy = strings.ToLower(config.GetConf().AssetLoadPolicy) h.displayBanner() - h.winWatchChan = make(chan bool) - h.loadDataDone = make(chan struct{}) + h.winWatchChan = make(chan bool, 5) + h.firstLoadDone = make(chan struct{}) go h.firstLoadData() } @@ -77,7 +87,7 @@ func (h *interactiveHandler) firstLoadData() { case "all": h.loadAllAssets() } - close(h.loadDataDone) + close(h.firstLoadDone) } func (h *interactiveHandler) displayBanner() { @@ -113,95 +123,13 @@ func (h *interactiveHandler) watchWinSizeChange() { } func (h *interactiveHandler) pauseWatchWinSize() { - select { - case <-h.sess.Sess.Context().Done(): - return - default: - } h.winWatchChan <- false } func (h *interactiveHandler) resumeWatchWinSize() { - select { - case <-h.sess.Sess.Context().Done(): - return - default: - } h.winWatchChan <- true } -func (h *interactiveHandler) Dispatch(ctx cctx.Context) { - go h.watchWinSizeChange() - defer logger.Infof("Request %s: User %s stop interactive", h.sess.ID(), h.user.Name) - for { - line, err := h.term.ReadLine() - if err != nil { - logger.Debugf("User %s close connect", h.user.Name) - break - } - line = strings.TrimSpace(line) - switch len(line) { - case 0, 1: - switch strings.ToLower(line) { - case "", "p": - // 展示所有的资产 - h.displayAllAssets() - case "g": - <-h.loadDataDone - h.displayNodes(h.nodes) - case "h": - h.displayBanner() - case "r": - h.refreshAssetsAndNodesData() - case "q": - logger.Debugf("user %s enter to exit", h.user.Name) - return - default: - h.searchAssetOrProxy(line) - } - default: - switch { - case line == "exit", line == "quit": - logger.Debugf("user %s enter to exit", h.user.Name) - return - case strings.Index(line, "/") == 0: - searchWord := strings.TrimSpace(line[1:]) - h.searchAsset(searchWord) - case strings.Index(line, "g") == 0: - searchWord := strings.TrimSpace(strings.TrimPrefix(line, "g")) - if num, err := strconv.Atoi(searchWord); err == nil { - if num >= 0 { - assets := h.searchNodeAssets(num) - h.displayAssets(assets) - continue - } - } - h.searchAssetOrProxy(line) - default: - h.searchAssetOrProxy(line) - } - } - - } -} - -func (h *interactiveHandler) displayAllAssets() { - switch h.assetLoadPolicy { - case "all": - <-h.loadDataDone - h.displayAssets(h.allAssets) - default: - pag := NewUserPagination(h.term, h.user.ID, "", false) - result := pag.Start() - if pag.IsNeedProxy && len(result) == 1 { - h.searchResult = h.searchResult[:0] - h.ProxyAsset(result[0]) - } else { - h.searchResult = result - } - } -} - func (h *interactiveHandler) chooseSystemUser(asset model.Asset, systemUsers []model.SystemUser) (systemUser model.SystemUser, ok bool) { @@ -269,50 +197,20 @@ func (h *interactiveHandler) chooseSystemUser(asset model.Asset, } } -func (h *interactiveHandler) displayAssets(assets model.AssetList) { - if len(assets) == 0 { - _, _ = io.WriteString(h.term, getI18nFromMap("NoAssets")+"\n\r") - } else { - sortedAssets := assets.SortBy(config.GetConf().AssetListSortBy) - pag := NewAssetPagination(h.term, sortedAssets) - selectOneAssets := pag.Start() - if len(selectOneAssets) == 1 { - systemUsers := service.GetUserAssetSystemUsers(h.user.ID, selectOneAssets[0].ID) - systemUser, ok := h.chooseSystemUser(selectOneAssets[0], systemUsers) - if !ok { - return - } - h.assetSelect = &selectOneAssets[0] - h.systemUserSelect = &systemUser - h.Proxy(context.TODO()) - } - if pag.page.PageSize() >= pag.page.TotalCount() { - h.searchResult = sortedAssets - } - } -} - -func (h *interactiveHandler) displayNodes(nodes []model.Node) { - tree := ConstructAssetNodeTree(nodes) - _, _ = io.WriteString(h.term, "\n\r"+getI18nFromMap("NodeHeaderTip")) - _, _ = io.WriteString(h.term, tree.String()) - _, err := io.WriteString(h.term, getI18nFromMap("NodeEndTip")+"\n\r") - if err != nil { - logger.Info("displayAssetNodes err:", err) - } - -} - func (h *interactiveHandler) refreshAssetsAndNodesData() { switch h.assetLoadPolicy { case "all": h.loadAllAssets() + default: + _ = service.ForceRefreshUserPemAssets(h.user.ID) } h.loadUserNodes("2") _, err := io.WriteString(h.term, getI18nFromMap("RefreshDone")+"\n\r") if err != nil { logger.Error("refresh Assets Nodes err:", err) } + h.assetPaginator = nil + h.dbPaginator = nil } func (h *interactiveHandler) loadUserNodes(cachePolicy string) { @@ -323,76 +221,6 @@ func (h *interactiveHandler) loadAllAssets() { h.allAssets = service.GetUserAllAssets(h.user.ID) } -func (h *interactiveHandler) searchAsset(key string) { - switch h.assetLoadPolicy { - case "all": - <-h.loadDataDone - var searchData []model.Asset - switch len(h.searchResult) { - case 0: - searchData = h.allAssets - default: - searchData = h.searchResult - } - assets := searchFromLocalAssets(searchData, key) - h.displayAssets(assets) - default: - pag := NewUserPagination(h.term, h.user.ID, key, false) - result := pag.Start() - if pag.IsNeedProxy && len(result) == 1 { - h.searchResult = h.searchResult[:0] - h.ProxyAsset(result[0]) - } else { - h.searchResult = result - } - } -} - -func (h *interactiveHandler) searchAssetOrProxy(key string) { - if indexNum, err := strconv.Atoi(key); err == nil && len(h.searchResult) > 0 { - if indexNum > 0 && indexNum <= len(h.searchResult) { - assetSelect := h.searchResult[indexNum-1] - h.ProxyAsset(assetSelect) - return - } - } - var assets []model.Asset - switch h.assetLoadPolicy { - case "all": - <-h.loadDataDone - var searchData []model.Asset - switch len(h.searchResult) { - case 0: - searchData = h.allAssets - default: - searchData = h.searchResult - } - assets = searchFromLocalAssets(searchData, key) - if len(assets) != 1 { - h.displayAssets(assets) - return - } - default: - pag := NewUserPagination(h.term, h.user.ID, key, true) - assets = pag.Start() - } - - if len(assets) == 1 { - h.ProxyAsset(assets[0]) - } else { - h.searchResult = assets - } -} - -func (h *interactiveHandler) searchNodeAssets(num int) (assets model.AssetList) { - if num > len(h.nodes) || num == 0 { - return assets - } - node := h.nodes[num-1] - assets = service.GetUserNodeAssets(h.user.ID, node.ID, "1") - return -} - func (h *interactiveHandler) ProxyAsset(assetSelect model.Asset) { systemUsers := service.GetUserAssetSystemUsers(h.user.ID, assetSelect.ID) systemUserSelect, ok := h.chooseSystemUser(assetSelect, systemUsers) @@ -414,6 +242,7 @@ func (h *interactiveHandler) Proxy(ctx context.Context) { h.pauseWatchWinSize() p.Proxy() h.resumeWatchWinSize() + logger.Infof("Request %s: asset %s proxy end", h.sess.Uuid, h.assetSelect.Hostname) } func ConstructAssetNodeTree(assetNodes []model.Node) treeprint.Tree { @@ -486,3 +315,29 @@ func searchFromLocalAssets(assets model.AssetList, key string) []model.Asset { } return displayAssets } + +func getPageSize(term *utils.Terminal) int { + var ( + pageSize int + minHeight = 8 // 分页显示的最小高度 + + ) + _, height := term.GetSize() + conf := config.GetConf() + switch conf.AssetListPageSize { + case "auto": + pageSize = height - minHeight + case "all": + return 0 + default: + if value, err := strconv.Atoi(conf.AssetListPageSize); err == nil { + pageSize = value + } else { + pageSize = height - minHeight + } + } + if pageSize <= 0 { + pageSize = 1 + } + return pageSize +} diff --git a/jumpserver/koko/pkg/handler/sftp.go b/jumpserver/koko/pkg/handler/sftp.go index 40d4883a5c5f8778b94a5a91b2b4f3add7112958..023ad6dd89a4df03a02dbc47069917b9e7d5d0db 100644 --- a/jumpserver/koko/pkg/handler/sftp.go +++ b/jumpserver/koko/pkg/handler/sftp.go @@ -12,18 +12,19 @@ import ( "github.com/pkg/sftp" uuid "github.com/satori/go.uuid" - "github.com/jumpserver/koko/pkg/cctx" "github.com/jumpserver/koko/pkg/logger" "github.com/jumpserver/koko/pkg/model" - "github.com/jumpserver/koko/pkg/service" "github.com/jumpserver/koko/pkg/srvconn" ) func SftpHandler(sess ssh.Session) { - ctx, cancel := cctx.NewContext(sess) - defer cancel() + currentUser, ok := sess.Context().Value(model.ContextKeyUser).(*model.User) + if !ok || currentUser.ID == "" { + logger.Errorf("SFTP User not found, exit.") + return + } host, _, _ := net.SplitHostPort(sess.RemoteAddr().String()) - userSftp := NewSFTPHandler(ctx.User(), host) + userSftp := NewSFTPHandler(currentUser, host) handlers := sftp.Handlers{ FileGet: userSftp, FilePut: userSftp, @@ -44,12 +45,11 @@ func SftpHandler(sess ssh.Session) { } func NewSFTPHandler(user *model.User, addr string) *sftpHandler { - assets := service.GetUserAllAssets(user.ID) - return &sftpHandler{srvconn.NewUserSFTP(user, addr, assets...)} + return &sftpHandler{UserSftpConn: srvconn.NewUserSftpConn(user, addr)} } type sftpHandler struct { - *srvconn.UserSftp + *srvconn.UserSftpConn } func (fs *sftpHandler) Filelist(r *sftp.Request) (sftp.ListerAt, error) { @@ -102,6 +102,16 @@ func (fs *sftpHandler) Filecmd(r *sftp.Request) (err error) { func (fs *sftpHandler) Filewrite(r *sftp.Request) (io.WriterAt, error) { logger.Debug("File write: ", r.Filepath) f, err := fs.Create(r.Filepath) + if err != nil { + return nil, err + } + go func() { + <-r.Context().Done() + if err := f.Close(); err != nil { + logger.Errorf("Remote sftp file %s close err: %s", r.Filepath, err) + } + logger.Infof("Sftp file write %s done", r.Filepath) + }() return NewWriterAt(f), err } @@ -116,11 +126,19 @@ func (fs *sftpHandler) Fileread(r *sftp.Request) (io.ReaderAt, error) { _ = f.Close() return nil, err } + go func() { + <-r.Context().Done() + if err := f.Close(); err != nil { + logger.Errorf("Remote sftp file %s close err: %s", r.Filepath, err) + } + logger.Infof("Sftp File read %s done", r.Filepath) + + }() return NewReaderAt(f, fi), err } func (fs *sftpHandler) Close() { - fs.UserSftp.Close() + fs.UserSftpConn.Close() } type listerat []os.FileInfo @@ -146,43 +164,26 @@ func NewReaderAt(f *sftp.File, fi os.FileInfo) io.ReaderAt { } type clientReadWritAt struct { - f *sftp.File - mu *sync.RWMutex - fi os.FileInfo - firstErr error + f *sftp.File + mu *sync.RWMutex + fi os.FileInfo } func (c *clientReadWritAt) WriteAt(p []byte, off int64) (n int, err error) { c.mu.Lock() defer c.mu.Unlock() - if c.firstErr != nil { - return 0, c.firstErr - } _, _ = c.f.Seek(off, 0) - nw, err := c.f.Write(p) - if err != nil { - c.firstErr = err - _ = c.f.Close() - } - return nw, err + return c.f.Write(p) } func (c *clientReadWritAt) ReadAt(p []byte, off int64) (n int, err error) { c.mu.Lock() defer c.mu.Unlock() - if c.firstErr != nil { - return 0, c.firstErr - } if off >= c.fi.Size() { return 0, io.EOF } _, _ = c.f.Seek(off, 0) - nr, err := c.f.Read(p) - if err != nil { - c.firstErr = err - _ = c.f.Close() - } - return nr, err + return c.f.Read(p) } type wrapperSFTPFileInfo struct { diff --git a/jumpserver/koko/pkg/handler/wrappersession.go b/jumpserver/koko/pkg/handler/wrappersession.go index ee85d44584292f9977fc40d4177bac6c86796d3a..7f2054322ec0e7c29662eafeb222a450ce080df7 100644 --- a/jumpserver/koko/pkg/handler/wrappersession.go +++ b/jumpserver/koko/pkg/handler/wrappersession.go @@ -66,7 +66,8 @@ func (w *WrapperSession) Close() error { return nil default: } - err := w.inWriter.Close() + _ = w.inWriter.Close() + err := w.outReader.Close() w.initReadPip() return err } diff --git a/jumpserver/koko/pkg/httpd/data.go b/jumpserver/koko/pkg/httpd/data.go index 9c25223d51ed0ec27945ac4ac788996222ec3cbf..3b7ce266b0891e639b7c17d011b13e8d5c51ed65 100644 --- a/jumpserver/koko/pkg/httpd/data.go +++ b/jumpserver/koko/pkg/httpd/data.go @@ -1,10 +1,11 @@ package httpd type HostMsg struct { - Uuid string `json:"uuid"` - UserID string `json:"userid"` - Secret string `json:"secret"` - Size []int `json:"size"` + Uuid string `json:"uuid"` + UserID string `json:"userid"` + Secret string `json:"secret"` + Size []int `json:"size"` + HostType string `json:"type"` } type ResizeMsg struct { diff --git a/jumpserver/koko/pkg/httpd/elfhandler.go b/jumpserver/koko/pkg/httpd/elfhandler.go index bd0b3bf401fe1a3241371af81e39f7c1618217e8..852a09b547e44e32fd388a617c532ff47e3caa3b 100644 --- a/jumpserver/koko/pkg/httpd/elfhandler.go +++ b/jumpserver/koko/pkg/httpd/elfhandler.go @@ -11,7 +11,6 @@ import ( "github.com/LeeEirc/elfinder" "github.com/gorilla/mux" - "github.com/jumpserver/koko/pkg/cctx" "github.com/jumpserver/koko/pkg/common" "github.com/jumpserver/koko/pkg/config" "github.com/jumpserver/koko/pkg/logger" @@ -45,8 +44,8 @@ func AuthDecorator(handler http.HandlerFunc) http.HandlerFunc { } else { remoteIP = strings.Split(request.RemoteAddr, ":")[0] } - ctx := context.WithValue(request.Context(), cctx.ContextKeyUser, user) - ctx = context.WithValue(ctx, cctx.ContextKeyRemoteAddr, remoteIP) + ctx := context.WithValue(request.Context(), model.ContextKeyUser, user) + ctx = context.WithValue(ctx, model.ContextKeyRemoteAddr, remoteIP) handler(responseWriter, request.WithContext(ctx)) } } @@ -66,8 +65,8 @@ func sftpFinder(wr http.ResponseWriter, req *http.Request) { func sftpHostConnectorView(wr http.ResponseWriter, req *http.Request) { vars := mux.Vars(req) hostID := vars["host"] - user := req.Context().Value(cctx.ContextKeyUser).(*model.User) - remoteIP := req.Context().Value(cctx.ContextKeyRemoteAddr).(string) + user := req.Context().Value(model.ContextKeyUser).(*model.User) + remoteIP := req.Context().Value(model.ContextKeyRemoteAddr).(string) switch req.Method { case "GET": if err := req.ParseForm(); err != nil { diff --git a/jumpserver/koko/pkg/httpd/sftpvolume.go b/jumpserver/koko/pkg/httpd/sftpvolume.go index ea944704ebb6a7ad08a474303c255d396413c9be..467ab5b74f24d61a2601a2f78ce77d47b79aaa31 100644 --- a/jumpserver/koko/pkg/httpd/sftpvolume.go +++ b/jumpserver/koko/pkg/httpd/sftpvolume.go @@ -7,6 +7,7 @@ import ( "path/filepath" "strings" "sync" + "time" "github.com/LeeEirc/elfinder" "github.com/pkg/sftp" @@ -18,26 +19,28 @@ import ( ) func NewUserVolume(user *model.User, addr, hostId string) *UserVolume { - var assets []model.Asset + var userSftp *srvconn.UserSftpConn homename := "Home" basePath := "/" switch hostId { case "": - assets = service.GetUserAllAssets(user.ID) + userSftp = srvconn.NewUserSftpConn(user, addr) default: - assets = service.GetUserAssetByID(user.ID, hostId) + assets := service.GetUserAssetByID(user.ID, hostId) if len(assets) == 1 { - homename = assets[0].Hostname - if assets[0].OrgID != "" { - homename = fmt.Sprintf("%s.%s", assets[0].Hostname, assets[0].OrgName) + folderName := assets[0].Hostname + if strings.Contains(folderName, "/") { + folderName = strings.ReplaceAll(folderName, "/", "_") } + homename = folderName basePath = filepath.Join("/", homename) } + userSftp = srvconn.NewUserSftpConnWithAssets(user, addr, assets...) } rawID := fmt.Sprintf("%s@%s", user.Username, addr) uVolume := &UserVolume{ Uuid: elfinder.GenerateID(rawID), - UserSftp: srvconn.NewUserSFTP(user, addr, assets...), + UserSftp: userSftp, Homename: homename, basePath: basePath, chunkFilesMap: make(map[int]*sftp.File), @@ -47,8 +50,8 @@ func NewUserVolume(user *model.User, addr, hostId string) *UserVolume { } type UserVolume struct { - Uuid string - *srvconn.UserSftp + Uuid string + UserSftp *srvconn.UserSftpConn Homename string basePath string @@ -66,7 +69,7 @@ func (u *UserVolume) Info(path string) (elfinder.FileDir, error) { if path == "/" { return u.RootFileDir(), nil } - originFileInfo, err := u.Stat(filepath.Join(u.basePath, path)) + originFileInfo, err := u.UserSftp.Stat(filepath.Join(u.basePath, path)) if err != nil { return rest, err } @@ -300,22 +303,44 @@ func (u *UserVolume) Paste(dir, filename, suffix string, reader io.ReadCloser) ( func (u *UserVolume) RootFileDir() elfinder.FileDir { logger.Debug("Root File Dir") - fInfo, _ := u.UserSftp.Stat(u.basePath) + var ( + size int64 + ) + tz := time.Now().UnixNano() + if fInfo, err := u.UserSftp.Stat(u.basePath); err == nil { + size = fInfo.Size() + tz = fInfo.ModTime().Unix() + } var rest elfinder.FileDir rest.Name = u.Homename rest.Hash = hashPath(u.Uuid, "/") - rest.Size = fInfo.Size() + rest.Size = size rest.Volumeid = u.Uuid rest.Mime = "directory" rest.Dirs = 1 rest.Read, rest.Write = 1, 1 rest.Locked = 1 - rest.Ts = fInfo.ModTime().Unix() + rest.Ts = tz return rest } func (u *UserVolume) Close() { u.UserSftp.Close() + logger.Infof("User %s's volume close", u.UserSftp.User.Name) +} + +func (u *UserVolume) Search(path, key string, mimes ...string) (res []elfinder.FileDir, err error) { + originFileInfolist, err := u.UserSftp.Search(key) + if err != nil { + return nil, err + } + res = make([]elfinder.FileDir, 0, len(originFileInfolist)) + searchPath := fmt.Sprintf("/%s", srvconn.SearchFolderName) + for i := 0; i < len(originFileInfolist); i++ { + res = append(res, NewElfinderFileInfo(u.Uuid, searchPath, originFileInfolist[i])) + + } + return } func NewElfinderFileInfo(id, dirPath string, originFileInfo os.FileInfo) elfinder.FileDir { diff --git a/jumpserver/koko/pkg/httpd/websshws.go b/jumpserver/koko/pkg/httpd/websshws.go index b794f65afe546dd338eca2082ed3d97a434ee175..6e5d6e79305c82b38aa0af7f77cb5e48c1541b0c 100644 --- a/jumpserver/koko/pkg/httpd/websshws.go +++ b/jumpserver/koko/pkg/httpd/websshws.go @@ -20,6 +20,10 @@ import ( "github.com/jumpserver/koko/pkg/service" ) +type proxyServer interface { + Proxy() +} + func OnPingHandler(c *neffos.NSConn, msg neffos.Message) error { c.Emit("pong", []byte("")) return nil @@ -106,18 +110,32 @@ func OnHostHandler(c *neffos.NSConn, msg neffos.Message) (err error) { emitMsg := RoomMsg{roomID, secret} roomMsg, _ := json.Marshal(emitMsg) c.Emit("room", roomMsg) + var databaseAsset model.Database + var asset model.Asset - asset := service.GetAsset(assetID) systemUser := service.GetSystemUser(systemUserID) + if message.HostType == "database" { + databaseAsset = service.GetDatabase(assetID) + if databaseAsset.ID == "" || systemUser.ID == "" { + msg := "No database id or system user id found, exit" + logger.Info(msg) + dataMsg := DataMsg{Room: roomID, Data: msg} + c.Emit("data", neffos.Marshal(dataMsg)) + return + } - if asset.ID == "" || systemUser.ID == "" { - msg := "No asset id or system user id found, exit" - logger.Debug(msg) - dataMsg := DataMsg{Room: roomID, Data: msg} - c.Emit("data", neffos.Marshal(dataMsg)) - return + logger.Infof("Web terminal want to connect database: %s", databaseAsset.Name) + } else { + asset = service.GetAsset(assetID) + if asset.ID == "" || systemUser.ID == "" { + msg := "No asset id or system user id found, exit" + logger.Debug(msg) + dataMsg := DataMsg{Room: roomID, Data: msg} + c.Emit("data", neffos.Marshal(dataMsg)) + return + } + logger.Infof("Web terminal want to connect host: %s", asset.Hostname) } - logger.Debug("Web terminal want to connect host: ", asset.Hostname) currentUser, ok := cc.Get("currentUser").(*model.User) if !ok { err = errors.New("not found current user") @@ -149,9 +167,20 @@ func OnHostHandler(c *neffos.NSConn, msg neffos.Message) (err error) { client.WinChan <- win clients.AddClient(roomID, client) conns.AddClient(cc.ID(), roomID) - proxySrv := proxy.ProxyServer{ - UserConn: client, User: currentUser, - Asset: &asset, SystemUser: &systemUser, + var proxySrv proxyServer + if message.HostType == "database" { + proxySrv = &proxy.DBProxyServer{ + UserConn: client, + User: currentUser, + Database: &databaseAsset, + SystemUser: &systemUser, + } + + } else { + proxySrv = &proxy.ProxyServer{ + UserConn: client, User: currentUser, + Asset: &asset, SystemUser: &systemUser, + } } go func() { defer logger.Infof("Request %s: Web ssh end proxy process", client.Uuid) diff --git a/jumpserver/koko/pkg/koko/koko.go b/jumpserver/koko/pkg/koko/koko.go index 153f0210145e4ad29217b0b96ee3b8941b38955e..77211b9000aebbb3019ced02e434fce7f3f028fd 100644 --- a/jumpserver/koko/pkg/koko/koko.go +++ b/jumpserver/koko/pkg/koko/koko.go @@ -16,7 +16,7 @@ import ( "github.com/jumpserver/koko/pkg/sshd" ) -const Version = "1.5.4" +const Version = "1.5.6" type Coco struct { } diff --git a/jumpserver/koko/pkg/model/assets.go b/jumpserver/koko/pkg/model/assets.go index 1352eb8654a945b440e5dbe81330f8da419b406a..d5ac8287ba37c1aa5e16a4bee1b606873ee898aa 100644 --- a/jumpserver/koko/pkg/model/assets.go +++ b/jumpserver/koko/pkg/model/assets.go @@ -90,7 +90,6 @@ type Asset struct { IP string `json:"ip"` Os string `json:"os"` Domain string `json:"domain"` - Platform string `json:"platform"` Comment string `json:"comment"` Protocols []string `json:"protocols,omitempty"` OrgID string `json:"org_id"` diff --git a/jumpserver/koko/pkg/model/const.go b/jumpserver/koko/pkg/model/const.go new file mode 100644 index 0000000000000000000000000000000000000000..de123b8f9702086dfb3552934c9cdb06d28c7287 --- /dev/null +++ b/jumpserver/koko/pkg/model/const.go @@ -0,0 +1,11 @@ +package model + +type contextKey int64 + +const ( + ContextKeyUser contextKey = iota + 1 + ContextKeyRemoteAddr + ContextKeyClient + ContextKeyConfirmRequired + ContextKeyConfirmFailed +) diff --git a/jumpserver/koko/pkg/model/database.go b/jumpserver/koko/pkg/model/database.go new file mode 100644 index 0000000000000000000000000000000000000000..97217a149652d387238e9eb2324c149572a9a4c6 --- /dev/null +++ b/jumpserver/koko/pkg/model/database.go @@ -0,0 +1,18 @@ +package model + +import "fmt" + +type Database struct { + ID string `json:"id"` + Name string `json:"name"` + DBType string `json:"type"` + Host string `json:"host"` + Port int `json:"port"` + DBName string `json:"database"` + OrgID string `json:"org_id"` + Comment string `json:"comment"` +} + +func (db Database) String() string { + return fmt.Sprintf("%s://%s:%d/%s", db.DBType, db.Host, db.Port, db.DBName) +} diff --git a/jumpserver/koko/pkg/model/nodetree.go b/jumpserver/koko/pkg/model/nodetree.go new file mode 100644 index 0000000000000000000000000000000000000000..3fee4af2384303c1da609a2e8a906dfef6170674 --- /dev/null +++ b/jumpserver/koko/pkg/model/nodetree.go @@ -0,0 +1,24 @@ +package model + +import "encoding/json" + +type NodeTreeList []NodeTreeAsset + +type NodeTreeAsset struct { + ID string `json:"id"` + Name string `json:"name"` + Title string `json:"title"` + Pid string `json:"pId"` + IsParent bool `json:"isParent"` + Meta map[string]interface{} `json:"meta"` +} + +func ConvertMetaToNode(body []byte) (node Node, err error) { + err = json.Unmarshal(body, &node) + return +} + +func ConvertMetaToAsset(body []byte) (asset Asset, err error) { + err = json.Unmarshal(body, &asset) + return +} diff --git a/jumpserver/koko/pkg/model/users.go b/jumpserver/koko/pkg/model/users.go index 05cb61af12197580871f7ac500d9cebf8743ca8c..f9904481509adbb92784f04d59fccf144a11a4c0 100644 --- a/jumpserver/koko/pkg/model/users.go +++ b/jumpserver/koko/pkg/model/users.go @@ -18,12 +18,6 @@ package model 'date_expired': '2089-03-21 18:18:24 +0800'} */ -type AuthResponse struct { - Token string `json:"token"` - Seed string `json:"seed"` - User *User `json:"user"` -} - type User struct { ID string `json:"id"` Name string `json:"name"` diff --git a/jumpserver/koko/pkg/proxy/dbparser.go b/jumpserver/koko/pkg/proxy/dbparser.go new file mode 100644 index 0000000000000000000000000000000000000000..bbed0d00ce1ee2620a6393069030823057dd659e --- /dev/null +++ b/jumpserver/koko/pkg/proxy/dbparser.go @@ -0,0 +1,217 @@ +package proxy + +import ( + "bytes" + "fmt" + "sync" + + "github.com/jumpserver/koko/pkg/i18n" + "github.com/jumpserver/koko/pkg/logger" + "github.com/jumpserver/koko/pkg/model" + "github.com/jumpserver/koko/pkg/utils" +) + +const ( + DBInputParserName = "DB Input parser" + DBOutputParserName = "DB Output parser" +) + +func newDBParser(id string) DBParser { + dbParser := DBParser{ + id: id, + } + dbParser.initial() + return dbParser +} + +type DBParser struct { + id string + + userOutputChan chan []byte + srvOutputChan chan []byte + cmdRecordChan chan [2]string + + inputInitial bool + inputPreState bool + inputState bool + once *sync.Once + lock *sync.RWMutex + + command string + output string + cmdInputParser *CmdParser + cmdOutputParser *CmdParser + + cmdFilterRules []model.SystemUserFilterRule + closed chan struct{} +} + +func (p *DBParser) initial() { + p.once = new(sync.Once) + p.lock = new(sync.RWMutex) + + p.cmdInputParser = NewCmdParser(p.id, DBInputParserName) + p.cmdOutputParser = NewCmdParser(p.id, DBOutputParserName) + + p.closed = make(chan struct{}) + p.cmdRecordChan = make(chan [2]string, 1024) +} + +// ParseStream 解析数据流 +func (p *DBParser) ParseStream(userInChan, srvInChan <-chan []byte) (userOut, srvOut <-chan []byte) { + + p.userOutputChan = make(chan []byte, 1) + p.srvOutputChan = make(chan []byte, 1) + logger.Infof("DB Session %s: Parser start", p.id) + go func() { + defer func() { + // 会话结束,结算命令结果 + p.sendCommandRecord() + close(p.cmdRecordChan) + close(p.userOutputChan) + close(p.srvOutputChan) + logger.Infof("DB Session %s: Parser routine done", p.id) + }() + for { + select { + case <-p.closed: + return + case b, ok := <-userInChan: + if !ok { + return + } + b = p.ParseUserInput(b) + select { + case <-p.closed: + return + case p.userOutputChan <- b: + } + + case b, ok := <-srvInChan: + if !ok { + return + } + b = p.ParseServerOutput(b) + select { + case <-p.closed: + return + case p.srvOutputChan <- b: + } + + } + } + }() + return p.userOutputChan, p.srvOutputChan +} + +// parseInputState 切换用户输入状态, 并结算命令和结果 +func (p *DBParser) parseInputState(b []byte) []byte { + p.inputPreState = p.inputState + if bytes.Contains(b, charEnter) { + // 连续输入enter key, 结算上一条可能存在的命令结果 + p.sendCommandRecord() + p.inputState = false + // 用户输入了Enter,开始结算命令 + p.parseCmdInput() + if cmd, ok := p.IsCommandForbidden(); !ok { + fbdMsg := utils.WrapperWarn(fmt.Sprintf(i18n.T("Command `%s` is forbidden"), cmd)) + _, _ = p.cmdOutputParser.WriteData([]byte(fbdMsg)) + p.srvOutputChan <- []byte("\r\n" + fbdMsg) + p.cmdRecordChan <- [2]string{p.command, fbdMsg} + p.command = "" + p.output = "" + return []byte{utils.CharCleanLine, '\r'} + } + } else { + p.inputState = true + // 用户又开始输入,并上次不处于输入状态,开始结算上次命令的结果 + if !p.inputPreState { + p.sendCommandRecord() + } + } + return b +} + +// parseCmdInput 解析命令的输入 +func (p *DBParser) parseCmdInput() { + p.command = p.cmdInputParser.Parse() +} + +// parseCmdOutput 解析命令输出 +func (p *DBParser) parseCmdOutput() { + p.output = p.cmdOutputParser.Parse() +} + +// ParseUserInput 解析用户的输入 +func (p *DBParser) ParseUserInput(b []byte) []byte { + p.lock.Lock() + defer p.lock.Unlock() + p.once.Do(func() { + p.inputInitial = true + }) + nb := p.parseInputState(b) + return nb +} + +// splitCmdStream 将服务器输出流分离到命令buffer和命令输出buffer +func (p *DBParser) splitCmdStream(b []byte) { + if !p.inputInitial { + return + } + if p.inputState { + _, _ = p.cmdInputParser.WriteData(b) + return + } + _, _ = p.cmdOutputParser.WriteData(b) +} + +// ParseServerOutput 解析服务器输出 +func (p *DBParser) ParseServerOutput(b []byte) []byte { + p.lock.Lock() + defer p.lock.Unlock() + p.splitCmdStream(b) + return b +} + +// SetCMDFilterRules 设置命令过滤规则 +func (p *DBParser) SetCMDFilterRules(rules []model.SystemUserFilterRule) { + p.cmdFilterRules = rules +} + +// IsCommandForbidden 判断命令是不是在过滤规则中 +func (p *DBParser) IsCommandForbidden() (string, bool) { + for _, rule := range p.cmdFilterRules { + allowed, cmd := rule.Match(p.command) + switch allowed { + case model.ActionAllow: + return "", true + case model.ActionDeny: + return cmd, false + default: + + } + } + return "", true +} + +// Close 关闭parser +func (p *DBParser) Close() { + select { + case <-p.closed: + return + default: + close(p.closed) + } + _ = p.cmdOutputParser.Close() + _ = p.cmdInputParser.Close() + logger.Infof("DB Session %s: Parser close", p.id) +} + +func (p *DBParser) sendCommandRecord() { + if p.command != "" { + p.parseCmdOutput() + p.cmdRecordChan <- [2]string{p.command, p.output} + p.command = "" + p.output = "" + } +} diff --git a/jumpserver/koko/pkg/proxy/dbproxy.go b/jumpserver/koko/pkg/proxy/dbproxy.go new file mode 100644 index 0000000000000000000000000000000000000000..a09d1e1f895923780604c4484c6e6b3717f5a049 --- /dev/null +++ b/jumpserver/koko/pkg/proxy/dbproxy.go @@ -0,0 +1,201 @@ +package proxy + +import ( + "fmt" + "strings" + "time" + + "github.com/jumpserver/koko/pkg/config" + "github.com/jumpserver/koko/pkg/i18n" + "github.com/jumpserver/koko/pkg/logger" + "github.com/jumpserver/koko/pkg/model" + "github.com/jumpserver/koko/pkg/service" + "github.com/jumpserver/koko/pkg/srvconn" + "github.com/jumpserver/koko/pkg/utils" +) + +type DBProxyServer struct { + UserConn UserConnection + User *model.User + Database *model.Database + SystemUser *model.SystemUser +} + +func (p *DBProxyServer) getAuthOrManualSet() error { + needManualSet := false + if p.SystemUser.LoginMode == model.LoginModeManual { + needManualSet = true + logger.Debugf("Database %s login mode is: %s", p.Database.Name, model.LoginModeManual) + } + if p.SystemUser.Password == "" { + needManualSet = true + logger.Debugf("Database %s neither has password", p.Database.Name) + } + if needManualSet { + term := utils.NewTerminal(p.UserConn, "password: ") + line, err := term.ReadPassword(fmt.Sprintf("%s's password: ", p.SystemUser.Username)) + if err != nil { + logger.Errorf("Get password from user err %s", err.Error()) + return err + } + p.SystemUser.Password = line + logger.Debug("Get password from user input: ", line) + } + return nil +} + +func (p *DBProxyServer) getUsernameIfNeed() (err error) { + if p.SystemUser.Username == "" { + var username string + term := utils.NewTerminal(p.UserConn, "username: ") + for { + username, err = term.ReadLine() + if err != nil { + return err + } + username = strings.TrimSpace(username) + if username != "" { + break + } + } + p.SystemUser.Username = username + logger.Debug("Get username from user input: ", username) + } + return +} + +func (p *DBProxyServer) checkProtocolMatch() bool { + return strings.ToLower(p.Database.DBType) == strings.ToLower(p.SystemUser.Protocol) +} + +func (p *DBProxyServer) checkProtocolClientInstalled() bool { + switch strings.ToLower(p.Database.DBType) { + case "mysql": + return utils.IsInstalledMysqlClient() + } + + return false +} + +// validatePermission 检查是否有权限连接 +func (p *DBProxyServer) validatePermission() bool { + return service.ValidateUserDatabasePermission(p.User.ID, p.Database.ID, p.SystemUser.ID) +} + +// getSSHConn 获取ssh连接 +func (p *DBProxyServer) getMysqlConn() (srvConn *srvconn.ServerMysqlConnection, err error) { + srvConn = srvconn.NewMysqlServer( + srvconn.SqlHost(p.Database.Host), + srvconn.SqlPort(p.Database.Port), + srvconn.SqlUsername(p.SystemUser.Username), + srvconn.SqlPassword(p.SystemUser.Password), + srvconn.SqlDBName(p.Database.DBName), + ) + err = srvConn.Connect() + return +} + +// getServerConn 获取获取server连接 +func (p *DBProxyServer) getServerConn() (srvConn srvconn.ServerConnection, err error) { + done := make(chan struct{}) + defer func() { + utils.IgnoreErrWriteString(p.UserConn, "\r\n") + close(done) + }() + go p.sendConnectingMsg(done, config.GetConf().SSHTimeout*time.Second) + return p.getMysqlConn() +} + +// sendConnectingMsg 发送连接信息 +func (p *DBProxyServer) sendConnectingMsg(done chan struct{}, delayDuration time.Duration) { + delay := 0.0 + msg := fmt.Sprintf(i18n.T("Database connecting to %s %.1f"), p.Database, delay) + utils.IgnoreErrWriteString(p.UserConn, msg) + for int(delay) < int(delayDuration/time.Second) { + select { + case <-done: + return + default: + delayS := fmt.Sprintf("%.1f", delay) + data := strings.Repeat("\x08", len(delayS)) + delayS + utils.IgnoreErrWriteString(p.UserConn, data) + time.Sleep(100 * time.Millisecond) + delay += 0.1 + } + } +} + +// preCheckRequisite 检查是否满足条件 +func (p *DBProxyServer) preCheckRequisite() (ok bool) { + if !p.checkProtocolMatch() { + msg := utils.WrapperWarn(i18n.T("System user <%s> and database <%s> protocol are inconsistent.")) + msg = fmt.Sprintf(msg, p.SystemUser.Username, p.Database.DBType) + utils.IgnoreErrWriteString(p.UserConn, msg) + return + } + if !p.checkProtocolClientInstalled() { + msg := utils.WrapperWarn(i18n.T("Database %s protocol client not installed.")) + msg = fmt.Sprintf(msg, p.Database.DBType) + utils.IgnoreErrWriteString(p.UserConn, msg) + return + } + if !p.validatePermission() { + msg := fmt.Sprintf("You don't have permission login %s", p.Database.Name) + utils.IgnoreErrWriteString(p.UserConn, msg) + return + } + if err := p.checkRequiredAuth(); err != nil { + msg := fmt.Sprintf("You get database %s auth info err: %s", p.Database.Name, err) + utils.IgnoreErrWriteString(p.UserConn, msg) + return + } + return true +} + +func (p *DBProxyServer) checkRequiredAuth() error { + info := service.GetSystemUserDatabaseAuthInfo(p.SystemUser.ID) + p.SystemUser.Password = info.Password + if err := p.getUsernameIfNeed(); err != nil { + logger.Errorf("Get database %s auth username err: %s", p.Database.Name, err) + return err + } + + if err := p.getAuthOrManualSet(); err != nil { + logger.Errorf("Get database %s auth password err: %s", p.Database.Name, err) + return err + } + return nil +} + +// sendConnectErrorMsg 发送连接错误消息 +func (p *DBProxyServer) sendConnectErrorMsg(err error) { + msg := fmt.Sprintf("Connect database %s error: %s\r\n", p.Database.Host, err) + utils.IgnoreErrWriteString(p.UserConn, msg) + logger.Error(msg) +} + +// Proxy 代理 +func (p *DBProxyServer) Proxy() { + if !p.preCheckRequisite() { + logger.Error("Check requisite failed") + return + } + // 创建Session + sw, err := CreateDBSession(p) + if err != nil { + logger.Error("Create database Session failed") + return + } + defer RemoveDBSession(sw) + srvConn, err := p.getServerConn() + // 连接后端服务器失败 + if err != nil { + logger.Errorf("Create database server conn failed: %s", err) + p.sendConnectErrorMsg(err) + return + } + if err = sw.Bridge(p.UserConn, srvConn); err != nil { + logger.Errorf("DB Session %s bridge end: %s", sw.ID, err) + } + +} diff --git a/jumpserver/koko/pkg/proxy/dbswitch.go b/jumpserver/koko/pkg/proxy/dbswitch.go new file mode 100644 index 0000000000000000000000000000000000000000..c72959fdc3e57c3f08ed8fbbb2c6764be9006772 --- /dev/null +++ b/jumpserver/koko/pkg/proxy/dbswitch.go @@ -0,0 +1,249 @@ +package proxy + +import ( + "context" + "fmt" + "io" + "strings" + "time" + + uuid "github.com/satori/go.uuid" + + "github.com/jumpserver/koko/pkg/common" + "github.com/jumpserver/koko/pkg/config" + "github.com/jumpserver/koko/pkg/i18n" + "github.com/jumpserver/koko/pkg/logger" + "github.com/jumpserver/koko/pkg/model" + "github.com/jumpserver/koko/pkg/srvconn" + "github.com/jumpserver/koko/pkg/utils" +) + +type DBSwitchSession struct { + ID string + p *DBProxyServer + + DateStart string + DateEnd string + finished bool + + MaxIdleTime time.Duration + + cmdRules []model.SystemUserFilterRule + + ctx context.Context + cancel context.CancelFunc +} + +func (s *DBSwitchSession) Initial() { + s.ID = uuid.NewV4().String() + s.DateStart = common.CurrentUTCTime() + s.MaxIdleTime = config.GetConf().MaxIdleTime + s.cmdRules = make([]model.SystemUserFilterRule, 0) + s.ctx, s.cancel = context.WithCancel(context.Background()) +} + +func (s *DBSwitchSession) Terminate() { + select { + case <-s.ctx.Done(): + return + default: + } + s.cancel() + logger.Infof("DBSession %s: receive terminate from admin", s.ID) +} + +func (s *DBSwitchSession) SessionID() string { + return s.ID +} + +func (s *DBSwitchSession) recordCommand(cmdRecordChan chan [2]string) { + // 命令记录 + cmdRecorder := NewCommandRecorder(s.ID) + for command := range cmdRecordChan { + if command[0] == "" { + continue + } + cmd := s.generateCommandResult(command) + cmdRecorder.Record(cmd) + } + // 关闭命令记录 + cmdRecorder.End() +} +func (s *DBSwitchSession) generateCommandResult(command [2]string) *model.Command { + var input string + var output string + if len(command[0]) > 128 { + input = command[0][:128] + } else { + input = command[0] + } + i := strings.LastIndexByte(command[1], '\r') + if i <= 0 { + output = command[1] + } else if i > 0 && i < 1024 { + output = command[1][:i] + } else { + output = command[1][:1024] + } + + return &model.Command{ + SessionID: s.ID, + OrgID: s.p.Database.OrgID, + Input: input, + Output: output, + User: fmt.Sprintf("%s (%s)", s.p.User.Name, s.p.User.Username), + Server: s.p.Database.Name, + SystemUser: s.p.SystemUser.Username, + Timestamp: time.Now().Unix(), + } +} + +func (s *DBSwitchSession) SetFilterRules(cmdRules []model.SystemUserFilterRule) { + if len(cmdRules) > 0 { + s.cmdRules = cmdRules + } +} + +// postBridge 桥接结束以后执行操作 +func (s *DBSwitchSession) postBridge() { + s.DateEnd = common.CurrentUTCTime() + s.finished = true +} + +// Bridge 桥接两个链接 +func (s *DBSwitchSession) Bridge(userConn UserConnection, srvConn srvconn.ServerConnection) (err error) { + var ( + parser DBParser + replayRecorder ReplyRecorder + + userInChan chan []byte + srvInChan chan []byte + done chan struct{} + ) + parser = newDBParser(s.ID) + replayRecorder = NewReplyRecord(s.ID) + + userInChan = make(chan []byte, 1) + srvInChan = make(chan []byte, 1) + done = make(chan struct{}) + // 设置parser的命令过滤规则 + parser.SetCMDFilterRules(s.cmdRules) + + userOutChan, srvOutChan := parser.ParseStream(userInChan, srvInChan) + defer func() { + close(done) + _ = userConn.Close() + _ = srvConn.Close() + // 关闭parser + parser.Close() + // 关闭录像 + replayRecorder.End() + s.postBridge() + }() + + go s.recordCommand(parser.cmdRecordChan) + go s.LoopReadFromSrv(done, srvConn, srvInChan) + go s.LoopReadFromUser(done, userConn, userInChan) + winCh := userConn.WinCh() + maxIdleTime := s.MaxIdleTime * time.Minute + lastActiveTime := time.Now() + tick := time.NewTicker(30 * time.Second) + defer tick.Stop() + for { + select { + // 检测是否超过最大空闲时间 + case <-tick.C: + now := time.Now() + outTime := lastActiveTime.Add(maxIdleTime) + if !now.After(outTime) { + continue + } + msg := fmt.Sprintf(i18n.T("Database connect idle more than %d minutes, disconnect"), s.MaxIdleTime) + logger.Infof("DB Session idle more than %d minutes, disconnect: %s", s.MaxIdleTime, s.ID) + msg = utils.WrapperWarn(msg) + utils.IgnoreErrWriteString(userConn, "\n\r"+msg) + return + // 手动结束 + case <-s.ctx.Done(): + msg := i18n.T("Database connection terminated by administrator") + msg = utils.WrapperWarn(msg) + logger.Infof("DBSession %s: %s", s.ID, msg) + utils.IgnoreErrWriteString(userConn, "\n\r"+msg) + return + // 监控窗口大小变化 + case win, ok := <-winCh: + if !ok { + return + } + _ = srvConn.SetWinSize(win.Height, win.Width) + logger.Debugf("DB Window server change: %d*%d", win.Height, win.Width) + // 经过parse处理的server数据,发给user + case p, ok := <-srvOutChan: + if !ok { + return + } + nw, _ := userConn.Write(p) + replayRecorder.Record(p[:nw]) + // 经过parse处理的user数据,发给server + case p, ok := <-userOutChan: + if !ok { + return + } + _, err = srvConn.Write(p) + } + lastActiveTime = time.Now() + } +} + +func (s *DBSwitchSession) MapData() map[string]interface{} { + var dataEnd interface{} + if s.DateEnd != "" { + dataEnd = s.DateEnd + } + return map[string]interface{}{ + "id": s.ID, + "user": fmt.Sprintf("%s (%s)", s.p.User.Name, s.p.User.Username), + "asset": s.p.Database.Name, + "org_id": s.p.Database.OrgID, + "login_from": s.p.UserConn.LoginFrom(), + "system_user": s.p.SystemUser.Username, + "protocol": s.p.SystemUser.Protocol, + "remote_addr": s.p.UserConn.RemoteAddr(), + "is_finished": s.finished, + "date_start": s.DateStart, + "date_end": dataEnd, + "user_id": s.p.User.ID, + "asset_id": s.p.Database.ID, + "system_user_id": s.p.SystemUser.ID, + } +} + +func (s *DBSwitchSession) LoopReadFromUser(done chan struct{}, userConn UserConnection, inChan chan<- []byte) { + defer logger.Infof("DB Session %s: read from user done", s.ID) + s.LoopRead(done, userConn, inChan) +} + +func (s *DBSwitchSession) LoopReadFromSrv(done chan struct{}, srvConn srvconn.ServerConnection, inChan chan<- []byte) { + defer logger.Infof("DB Session %s: read from srv done", s.ID) + s.LoopRead(done, srvConn, inChan) +} + +func (s *DBSwitchSession) LoopRead(done chan struct{}, read io.Reader, inChan chan<- []byte) { +loop: + for { + buf := make([]byte, 1024) + nr, err := read.Read(buf) + if nr > 0 { + select { + case <-done: + logger.Debug("DB session reader loop break done.") + break loop + case inChan <- buf[:nr]: + } + } + if err != nil { + break + } + } + close(inChan) +} diff --git a/jumpserver/koko/pkg/proxy/parser.go b/jumpserver/koko/pkg/proxy/parser.go index 75b0b5d002a31184ef2381c52786f1f4f85b7c27..83a9ecc9e4cf447deda8e39f8aee45cc46c3883d 100644 --- a/jumpserver/koko/pkg/proxy/parser.go +++ b/jumpserver/koko/pkg/proxy/parser.go @@ -68,7 +68,6 @@ func (p *Parser) initial() { p.cmdInputParser = NewCmdParser(p.id, CommandInputParserName) p.cmdOutputParser = NewCmdParser(p.id, CommandOutputParserName) - p.closed = make(chan struct{}) p.cmdRecordChan = make(chan [2]string, 1024) } @@ -86,8 +85,6 @@ func (p *Parser) ParseStream(userInChan, srvInChan <-chan []byte) (userOut, srvO close(p.cmdRecordChan) close(p.userOutputChan) close(p.srvOutputChan) - _ = p.cmdOutputParser.Close() - _ = p.cmdInputParser.Close() logger.Infof("Session %s: Parser routine done", p.id) }() for { @@ -99,13 +96,23 @@ func (p *Parser) ParseStream(userInChan, srvInChan <-chan []byte) (userOut, srvO return } b = p.ParseUserInput(b) - p.userOutputChan <- b + select { + case <-p.closed: + return + case p.userOutputChan <- b: + } + case b, ok := <-srvInChan: if !ok { return } b = p.ParseServerOutput(b) - p.srvOutputChan <- b + select { + case <-p.closed: + return + case p.srvOutputChan <- b: + } + } } }() @@ -120,6 +127,7 @@ func (p *Parser) parseInputState(b []byte) []byte { return b } p.inputPreState = p.inputState + if bytes.Contains(b, charEnter) { // 连续输入enter key, 结算上一条可能存在的命令结果 p.sendCommandRecord() @@ -128,7 +136,7 @@ func (p *Parser) parseInputState(b []byte) []byte { p.parseCmdInput() if cmd, ok := p.IsCommandForbidden(); !ok { fbdMsg := utils.WrapperWarn(fmt.Sprintf(i18n.T("Command `%s` is forbidden"), cmd)) - p.cmdOutputParser.WriteData([]byte(fbdMsg)) + _, _ = p.cmdOutputParser.WriteData([]byte(fbdMsg)) p.srvOutputChan <- []byte("\r\n" + fbdMsg) p.cmdRecordChan <- [2]string{p.command, fbdMsg} p.command = "" @@ -261,6 +269,9 @@ func (p *Parser) Close() { close(p.closed) } + _ = p.cmdOutputParser.Close() + _ = p.cmdInputParser.Close() + logger.Infof("Session %s: Parser close", p.id) } func (p *Parser) sendCommandRecord() { diff --git a/jumpserver/koko/pkg/proxy/parsercmd.go b/jumpserver/koko/pkg/proxy/parsercmd.go index bb0d040c1eca0808ff87417de9c1056f182e93c2..9399a2a684da8ac77b12a2405b6b7384b86114d0 100644 --- a/jumpserver/koko/pkg/proxy/parsercmd.go +++ b/jumpserver/koko/pkg/proxy/parsercmd.go @@ -1,7 +1,7 @@ package proxy import ( - "io" + "bytes" "regexp" "strings" "sync" @@ -21,87 +21,29 @@ func NewCmdParser(sid, name string) *CmdParser { type CmdParser struct { id string name string + buf bytes.Buffer - term *utils.Terminal - reader io.ReadCloser - writer io.WriteCloser - currentLines []string lock *sync.Mutex maxLength int currentLength int - closed chan struct{} } func (cp *CmdParser) WriteData(p []byte) (int, error) { - select { - case <-cp.closed: - return 0, io.EOF - default: - } - return cp.writer.Write(p) -} - -func (cp *CmdParser) Write(p []byte) (int, error) { - select { - case <-cp.closed: - return 0, io.EOF - default: - } - return len(p), nil -} - -func (cp *CmdParser) Read(p []byte) (int, error) { - select { - case <-cp.closed: - return 0, io.EOF - default: + cp.lock.Lock() + defer cp.lock.Unlock() + if cp.buf.Len() >= 1024 { + return 0, nil } - return cp.reader.Read(p) + return cp.buf.Write(p) } func (cp *CmdParser) Close() error { - select { - case <-cp.closed: - return nil - default: - close(cp.closed) - } - _ = cp.reader.Close() - return cp.writer.Close() + logger.Infof("session ID: %s, parser name: %s Close", cp.id, cp.name) + return nil } func (cp *CmdParser) initial() { - cp.reader, cp.writer = io.Pipe() - cp.currentLines = make([]string, 0) cp.lock = new(sync.Mutex) - cp.maxLength = 1024 - cp.currentLength = 0 - cp.closed = make(chan struct{}) - - cp.term = utils.NewTerminal(cp, "") - cp.term.SetEcho(false) - go func() { - logger.Infof("Session %s: %s start", cp.id, cp.name) - defer logger.Infof("Session %s: %s parser close", cp.id, cp.name) - loop: - for { - line, err := cp.term.ReadLine() - if err != nil { - select { - case <-cp.closed: - break loop - default: - } - goto loop - } - cp.lock.Lock() - cp.currentLength += len(line) - if cp.currentLength < cp.maxLength { - cp.currentLines = append(cp.currentLines, line) - } - cp.lock.Unlock() - } - }() } func (cp *CmdParser) parsePS1(s string) string { @@ -110,16 +52,11 @@ func (cp *CmdParser) parsePS1(s string) string { // Parse 解析命令或输出 func (cp *CmdParser) Parse() string { - select { - case <-cp.closed: - default: - cp.writer.Write([]byte("\r")) - } cp.lock.Lock() defer cp.lock.Unlock() - output := strings.TrimSpace(strings.Join(cp.currentLines, "\r\n")) + lines := utils.ParseTerminalData(cp.buf.Bytes()) + output := strings.TrimSpace(strings.Join(lines, "\r\n")) output = cp.parsePS1(output) - cp.currentLines = make([]string, 0) - cp.currentLength = 0 + cp.buf.Reset() return output } diff --git a/jumpserver/koko/pkg/proxy/proxy.go b/jumpserver/koko/pkg/proxy/proxy.go index bd19a7f930acf73320c2225f142c121aff1d0886..8badffa88b9b9186456c61376175692ea49d562e 100644 --- a/jumpserver/koko/pkg/proxy/proxy.go +++ b/jumpserver/koko/pkg/proxy/proxy.go @@ -116,7 +116,10 @@ func (p *ProxyServer) getSSHConn() (srvConn *srvconn.ServerSSHConnection, err er func (p *ProxyServer) getTelnetConn() (srvConn *srvconn.ServerTelnetConnection, err error) { conf := config.GetConf() cusString := conf.TelnetRegex - pattern, _ := regexp.Compile(cusString) + pattern, err := regexp.Compile(cusString) + if err != nil { + logger.Errorf("telnet custom regex %s compile err: %s", cusString, err) + } srvConn = &srvconn.ServerTelnetConnection{ User: p.User, Asset: p.Asset, diff --git a/jumpserver/koko/pkg/proxy/recorder.go b/jumpserver/koko/pkg/proxy/recorder.go index 4ab3aac9cbf4a8f0abb66ce98752be6142550306..1e463551bd0e9f14709ad26e0cd8e80eca4b48fd 100644 --- a/jumpserver/koko/pkg/proxy/recorder.go +++ b/jumpserver/koko/pkg/proxy/recorder.go @@ -139,7 +139,7 @@ func (r *ReplyRecorder) prepare() { return } - logger.Infof("Session %s: Replay file path: %s",r.SessionID, r.absFilePath) + logger.Infof("Session %s: Replay file path: %s", r.SessionID, r.absFilePath) r.file, err = os.Create(r.absFilePath) if err != nil { logger.Errorf("Create file %s error: %s\n", r.absFilePath, err) @@ -176,9 +176,14 @@ func (r *ReplyRecorder) uploadReplay() { func (r *ReplyRecorder) UploadGzipFile(maxRetry int) { if r.storage == nil { - r.backOffStorage = defaultReplayStorage + r.backOffStorage = defaultStorage r.storage = NewReplayStorage() } + if r.storage.TypeName() == "null" { + _ = r.storage.Upload(r.AbsGzFilePath, r.Target) + _ = os.Remove(r.AbsGzFilePath) + return + } for i := 0; i <= maxRetry; i++ { logger.Debug("Upload replay file: ", r.AbsGzFilePath) err := r.storage.Upload(r.AbsGzFilePath, r.Target) diff --git a/jumpserver/koko/pkg/proxy/recorderstorage/azure.go b/jumpserver/koko/pkg/proxy/recorderstorage/azure.go index cc98cb91260cb22a519bf04c702d0b2f8d1cc8d1..0db5d17aaad90716ee03043ef99faea8c9c77656 100644 --- a/jumpserver/koko/pkg/proxy/recorderstorage/azure.go +++ b/jumpserver/koko/pkg/proxy/recorderstorage/azure.go @@ -42,3 +42,7 @@ func (a AzureReplayStorage) Upload(gZipFilePath, target string) (err error) { } return } + +func (o AzureReplayStorage) TypeName() string { + return "azure" +} diff --git a/jumpserver/koko/pkg/proxy/recorderstorage/es.go b/jumpserver/koko/pkg/proxy/recorderstorage/es.go index c0716df7c2490794754f5be62ae587c8b6ab0559..15c55b65b107409d3233a8d833d50c45d55e1a54 100644 --- a/jumpserver/koko/pkg/proxy/recorderstorage/es.go +++ b/jumpserver/koko/pkg/proxy/recorderstorage/es.go @@ -5,7 +5,7 @@ import ( "encoding/json" "fmt" - "github.com/elastic/go-elasticsearch" + "github.com/elastic/go-elasticsearch/v6" "github.com/jumpserver/koko/pkg/logger" "github.com/jumpserver/koko/pkg/model" @@ -44,3 +44,7 @@ func (es ESCommandStorage) BulkSave(commands []*model.Command) (err error) { } return } + +func (f ESCommandStorage) TypeName() string { + return "es" +} \ No newline at end of file diff --git a/jumpserver/koko/pkg/proxy/recorderstorage/file.go b/jumpserver/koko/pkg/proxy/recorderstorage/file.go deleted file mode 100644 index d07a1042c7a6cc403b8bfc4f6df8659e8a43f5c6..0000000000000000000000000000000000000000 --- a/jumpserver/koko/pkg/proxy/recorderstorage/file.go +++ /dev/null @@ -1,30 +0,0 @@ -package recorderstorage - -import ( - "fmt" - "os" - - "github.com/jumpserver/koko/pkg/model" -) - -func NewFileCommandStorage(name string) (storage *FileCommandStorage, err error) { - file, err := os.Create(name) - if err != nil { - return - } - storage = &FileCommandStorage{File: file} - return -} - -type FileCommandStorage struct { - File *os.File -} - -func (f *FileCommandStorage) BulkSave(commands []*model.Command) (err error) { - for _, cmd := range commands { - f.File.WriteString(fmt.Sprintf("命令: %s\n", cmd.Input)) - f.File.WriteString(fmt.Sprintf("结果: %s\n", cmd.Output)) - f.File.WriteString("---\n") - } - return -} diff --git a/jumpserver/koko/pkg/proxy/recorderstorage/null.go b/jumpserver/koko/pkg/proxy/recorderstorage/null.go new file mode 100644 index 0000000000000000000000000000000000000000..5b22fe5a657059238894aa25be2851a29cb796a5 --- /dev/null +++ b/jumpserver/koko/pkg/proxy/recorderstorage/null.go @@ -0,0 +1,28 @@ +package recorderstorage + +import ( + "github.com/jumpserver/koko/pkg/logger" + "github.com/jumpserver/koko/pkg/model" +) + +func NewNullStorage() (storage NullStorage) { + storage = NullStorage{} + return +} + +type NullStorage struct { +} + +func (f NullStorage) BulkSave(commands []*model.Command) (err error) { + logger.Infof("Null Storage discard %d commands.", len(commands)) + return +} + +func (f NullStorage) Upload(gZipFile, target string) (err error) { + logger.Infof("Null Storage discard %s.", gZipFile) + return +} + +func (f NullStorage) TypeName() string { + return "null" +} diff --git a/jumpserver/koko/pkg/proxy/recorderstorage/oss.go b/jumpserver/koko/pkg/proxy/recorderstorage/oss.go index 464643511c19efa354597f61edd317360f560709..661cb929eeab38ed30df256e51443c5249c87550 100644 --- a/jumpserver/koko/pkg/proxy/recorderstorage/oss.go +++ b/jumpserver/koko/pkg/proxy/recorderstorage/oss.go @@ -25,3 +25,7 @@ func (o OSSReplayStorage) Upload(gZipFilePath, target string) (err error) { } return bucket.PutObjectFromFile(target, gZipFilePath) } + +func (o OSSReplayStorage) TypeName() string { + return "oss" +} diff --git a/jumpserver/koko/pkg/proxy/recorderstorage/s3.go b/jumpserver/koko/pkg/proxy/recorderstorage/s3.go index 2c5621bc70ffe8e4347b5028d697be894fde3d5e..c56cd3cd6a207da52bab0737d10b0c205badf548 100644 --- a/jumpserver/koko/pkg/proxy/recorderstorage/s3.go +++ b/jumpserver/koko/pkg/proxy/recorderstorage/s3.go @@ -28,9 +28,10 @@ func (s S3ReplayStorage) Upload(gZipFilePath, target string) (err error) { } defer file.Close() s3Config := &aws.Config{ - Credentials: credentials.NewStaticCredentials(s.AccessKey, s.SecretKey, ""), - Endpoint: aws.String(s.Endpoint), - Region: aws.String(s.Region), + Credentials: credentials.NewStaticCredentials(s.AccessKey, s.SecretKey, ""), + Endpoint: aws.String(s.Endpoint), + Region: aws.String(s.Region), + S3ForcePathStyle: aws.Bool(true), } sess := session.Must(session.NewSession(s3Config)) @@ -48,3 +49,8 @@ func (s S3ReplayStorage) Upload(gZipFilePath, target string) (err error) { return } + + +func (s S3ReplayStorage) TypeName() string { + return "s3" +} \ No newline at end of file diff --git a/jumpserver/koko/pkg/proxy/recorderstorage/server.go b/jumpserver/koko/pkg/proxy/recorderstorage/server.go index 3a289ffcbf28d8bd4e42b36279c448382bdb5499..93a335bc45059ed49229209addb1f062decd7f07 100644 --- a/jumpserver/koko/pkg/proxy/recorderstorage/server.go +++ b/jumpserver/koko/pkg/proxy/recorderstorage/server.go @@ -8,18 +8,19 @@ import ( "github.com/jumpserver/koko/pkg/service" ) -type ServerCommandStorage struct { +type ServerStorage struct { + StorageType string } -func (s ServerCommandStorage) BulkSave(commands []*model.Command) (err error) { +func (s ServerStorage) BulkSave(commands []*model.Command) (err error) { return service.PushSessionCommand(commands) } -type ServerReplayStorage struct { - StorageType string -} - -func (s ServerReplayStorage) Upload(gZipFilePath, target string) (err error) { +func (s ServerStorage) Upload(gZipFilePath, target string) (err error) { sessionID := strings.Split(filepath.Base(gZipFilePath), ".")[0] return service.PushSessionReplay(sessionID, gZipFilePath) } + +func (s ServerStorage) TypeName() string { + return s.StorageType +} diff --git a/jumpserver/koko/pkg/proxy/sessmanager.go b/jumpserver/koko/pkg/proxy/sessmanager.go index 6460e3d39b84234a13b2a391866af68a9edd78ba..c74deb0d01db914e80e7ee07548993403237493f 100644 --- a/jumpserver/koko/pkg/proxy/sessmanager.go +++ b/jumpserver/koko/pkg/proxy/sessmanager.go @@ -12,9 +12,14 @@ import ( "github.com/jumpserver/koko/pkg/utils" ) -var sessionMap = make(map[string]*SwitchSession) +var sessionMap = make(map[string]Session) var lock = new(sync.RWMutex) +type Session interface { + SessionID() string + Terminate() +} + func HandleSessionTask(task model.TerminalTask) { switch task.Name { case "kill_session": @@ -50,20 +55,23 @@ func RemoveSession(sw *SwitchSession) { lock.Lock() defer lock.Unlock() delete(sessionMap, sw.ID) - finishSession(sw) + data := sw.MapData() + finishSession(data) + logger.Infof("Session %s has finished", sw.ID) } -func AddSession(sw *SwitchSession) { +func AddSession(sw Session) { lock.Lock() defer lock.Unlock() - sessionMap[sw.ID] = sw + sessionMap[sw.SessionID()] = sw } func CreateSession(p *ProxyServer) (sw *SwitchSession, err error) { // 创建Session sw = NewSwitchSession(p) // Post到Api端 - ok := postSession(sw) + data := sw.MapData() + ok := postSession(data) msg := i18n.T("Connect with api server failed") if !ok { msg = utils.WrapperWarn(msg) @@ -84,8 +92,7 @@ func CreateSession(p *ProxyServer) (sw *SwitchSession, err error) { return } -func postSession(s *SwitchSession) bool { - data := s.MapData() +func postSession(data map[string]interface{}) bool { for i := 0; i < 5; i++ { if service.CreateSession(data) { return true @@ -95,8 +102,41 @@ func postSession(s *SwitchSession) bool { return false } -func finishSession(s *SwitchSession) { - data := s.MapData() +func finishSession(data map[string]interface{}) { service.FinishSession(data) - logger.Debugf("Session %s has finished", s.ID) +} + +func CreateDBSession(p *DBProxyServer) (sw *DBSwitchSession, err error) { + // 创建Session + sw = &DBSwitchSession{ + p: p, + } + sw.Initial() + data := sw.MapData() + ok := postSession(data) + msg := i18n.T("Create database session failed") + if !ok { + msg = utils.WrapperWarn(msg) + utils.IgnoreErrWriteString(p.UserConn, msg) + logger.Error(msg) + return sw, errors.New("create database session failed") + } + cmdRules, err := service.GetSystemUserFilterRules(p.SystemUser.ID) + if err != nil { + msg = utils.WrapperWarn(msg) + utils.IgnoreErrWriteString(p.UserConn, msg) + logger.Error(msg + err.Error()) + return sw, errors.New("connect api server failed") + } + sw.SetFilterRules(cmdRules) + AddSession(sw) + return +} + +func RemoveDBSession(sw *DBSwitchSession) { + lock.Lock() + defer lock.Unlock() + delete(sessionMap, sw.ID) + finishSession(sw.MapData()) + logger.Infof("DB Session %s has finished", sw.ID) } diff --git a/jumpserver/koko/pkg/proxy/switch.go b/jumpserver/koko/pkg/proxy/switch.go index 00331b3763d3db09957051a7e131c3ef32999569..e3f364cd6362adda56e038f86773b55029a3e535 100644 --- a/jumpserver/koko/pkg/proxy/switch.go +++ b/jumpserver/koko/pkg/proxy/switch.go @@ -55,6 +55,11 @@ func (s *SwitchSession) Terminate() { default: } s.cancel() + logger.Infof("Session %s: receive terminate from admin", s.ID) +} + +func (s *SwitchSession) SessionID() string { + return s.ID } func (s *SwitchSession) recordCommand(cmdRecordChan chan [2]string) { @@ -122,14 +127,15 @@ func (s *SwitchSession) Bridge(userConn UserConnection, srvConn srvconn.ServerCo userInChan chan []byte srvInChan chan []byte + done chan struct{} ) parser = newParser(s.ID) replayRecorder = NewReplyRecord(s.ID) - userInChan = make(chan []byte, 10) - srvInChan = make(chan []byte, 10) - + userInChan = make(chan []byte, 1) + srvInChan = make(chan []byte, 1) + done = make(chan struct{}) // 设置parser的命令过滤规则 parser.SetCMDFilterRules(s.cmdRules) @@ -137,6 +143,7 @@ func (s *SwitchSession) Bridge(userConn UserConnection, srvConn srvconn.ServerCo userOutChan, srvOutChan := parser.ParseStream(userInChan, srvInChan) defer func() { + close(done) _ = userConn.Close() _ = srvConn.Close() // 关闭parser @@ -148,9 +155,8 @@ func (s *SwitchSession) Bridge(userConn UserConnection, srvConn srvconn.ServerCo // 记录命令 go s.recordCommand(parser.cmdRecordChan) - go LoopRead(userConn, userInChan) - go LoopRead(srvConn, srvInChan) - + go s.LoopReadFromSrv(done, srvConn, srvInChan) + go s.LoopReadFromUser(done, userConn, userInChan) winCh := userConn.WinCh() maxIdleTime := s.MaxIdleTime * time.Minute lastActiveTime := time.Now() @@ -174,6 +180,7 @@ func (s *SwitchSession) Bridge(userConn UserConnection, srvConn srvconn.ServerCo case <-s.ctx.Done(): msg := i18n.T("Terminated by administrator") msg = utils.WrapperWarn(msg) + logger.Infof("Session %s: %s", s.ID, msg) utils.IgnoreErrWriteString(userConn, "\n\r"+msg) return // 监控窗口大小变化 @@ -209,27 +216,44 @@ func (s *SwitchSession) MapData() map[string]interface{} { dataEnd = s.DateEnd } return map[string]interface{}{ - "id": s.ID, - "user": fmt.Sprintf("%s (%s)", s.p.User.Name, s.p.User.Username), - "asset": s.p.Asset.Hostname, - "org_id": s.p.Asset.OrgID, - "login_from": s.p.UserConn.LoginFrom(), - "system_user": s.p.SystemUser.Username, - "protocol": s.p.SystemUser.Protocol, - "remote_addr": s.p.UserConn.RemoteAddr(), - "is_finished": s.finished, - "date_start": s.DateStart, - "date_end": dataEnd, + "id": s.ID, + "user": fmt.Sprintf("%s (%s)", s.p.User.Name, s.p.User.Username), + "asset": s.p.Asset.Hostname, + "org_id": s.p.Asset.OrgID, + "login_from": s.p.UserConn.LoginFrom(), + "system_user": s.p.SystemUser.Username, + "protocol": s.p.SystemUser.Protocol, + "remote_addr": s.p.UserConn.RemoteAddr(), + "is_finished": s.finished, + "date_start": s.DateStart, + "date_end": dataEnd, + "user_id": s.p.User.ID, + "asset_id": s.p.Asset.ID, + "system_user_id": s.p.SystemUser.ID, } } -func LoopRead(read io.Reader, inChan chan<- []byte) { - defer logger.Debug("loop read end") +func (s *SwitchSession) LoopReadFromUser(done chan struct{}, userConn UserConnection, inChan chan<- []byte) { + defer logger.Infof("Session %s: read from user done", s.ID) + s.LoopRead(done, userConn, inChan) +} + +func (s *SwitchSession) LoopReadFromSrv(done chan struct{}, srvConn srvconn.ServerConnection, inChan chan<- []byte) { + defer logger.Infof("Session %s: read from srv done", s.ID) + s.LoopRead(done, srvConn, inChan) +} + +func (s *SwitchSession) LoopRead(done chan struct{}, read io.Reader, inChan chan<- []byte) { +loop: for { buf := make([]byte, 1024) nr, err := read.Read(buf) if nr > 0 { - inChan <- buf[:nr] + select { + case <-done: + break loop + case inChan <- buf[:nr]: + } } if err != nil { break diff --git a/jumpserver/koko/pkg/proxy/util.go b/jumpserver/koko/pkg/proxy/util.go index 7732ea5239dcf5a42e6cb8b004c7f75e5db39328..ca9c4c6bfb3bc6d51060f9988a6d018b54c6c159 100644 --- a/jumpserver/koko/pkg/proxy/util.go +++ b/jumpserver/koko/pkg/proxy/util.go @@ -8,16 +8,21 @@ import ( storage "github.com/jumpserver/koko/pkg/proxy/recorderstorage" ) +type StorageType interface { + TypeName() string +} + type ReplayStorage interface { Upload(gZipFile, target string) error + StorageType } type CommandStorage interface { BulkSave(commands []*model.Command) error + StorageType } -var defaultCommandStorage = storage.ServerCommandStorage{} -var defaultReplayStorage = storage.ServerReplayStorage{StorageType: "server"} +var defaultStorage = storage.ServerStorage{StorageType: "server"} func NewReplayStorage() ReplayStorage { cf := config.GetConf().ReplayStorage @@ -27,46 +32,96 @@ func NewReplayStorage() ReplayStorage { } switch tp { case "azure": - endpointSuffix := cf["ENDPOINT_SUFFIX"].(string) + var accountName string + var accountKey string + var containerName string + var endpointSuffix string + if value, ok := cf["ENDPOINT_SUFFIX"].(string); ok { + endpointSuffix = value + } + if value, ok := cf["ACCOUNT_NAME"].(string); ok { + accountName = value + } + if value, ok := cf["ACCOUNT_KEY"].(string); ok { + accountKey = value + } + if value, ok := cf["CONTAINER_NAME"].(string); ok { + containerName = value + } if endpointSuffix == "" { endpointSuffix = "core.chinacloudapi.cn" } return storage.AzureReplayStorage{ - AccountName: cf["ACCOUNT_NAME"].(string), - AccountKey: cf["ACCOUNT_KEY"].(string), - ContainerName: cf["CONTAINER_NAME"].(string), + AccountName: accountName, + AccountKey: accountKey, + ContainerName: containerName, EndpointSuffix: endpointSuffix, } case "oss": + var endpoint string + var bucket string + var accessKey string + var secretKey string + + if value, ok := cf["ENDPOINT"].(string); ok { + endpoint = value + } + if value, ok := cf["BUCKET"].(string); ok { + bucket = value + } + if value, ok := cf["ACCESS_KEY"].(string); ok { + accessKey = value + } + if value, ok := cf["SECRET_KEY"].(string); ok { + secretKey = value + } return storage.OSSReplayStorage{ - Endpoint: cf["ENDPOINT"].(string), - Bucket: cf["BUCKET"].(string), - AccessKey: cf["ACCESS_KEY"].(string), - SecretKey: cf["SECRET_KEY"].(string), + Endpoint: endpoint, + Bucket: bucket, + AccessKey: accessKey, + SecretKey: secretKey, } - case "s3": + case "s3", "swift": var region string var endpoint string - bucket := cf["BUCKET"].(string) - endpoint = cf["ENDPOINT"].(string) + var bucket string + var accessKey string + var secretKey string + if value, ok := cf["BUCKET"].(string); ok { + bucket = value + } + if value, ok := cf["ENDPOINT"].(string); ok { + endpoint = value + } + if value, ok := cf["REGION"].(string); ok { + region = value + } + if value, ok := cf["ACCESS_KEY"].(string); ok { + accessKey = value + } + if value, ok := cf["SECRET_KEY"].(string); ok { + secretKey = value + } + if region == "" && endpoint != "" { + endpointArray := strings.Split(endpoint, ".") + if len(endpointArray) >= 2 { + region = endpointArray[1] + } + } if bucket == "" { bucket = "jumpserver" } - if cf["REGION"] != nil { - region = cf["REGION"].(string) - } else { - region = strings.Split(endpoint, ".")[1] - } - return storage.S3ReplayStorage{ Bucket: bucket, Region: region, - AccessKey: cf["ACCESS_KEY"].(string), - SecretKey: cf["SECRET_KEY"].(string), + AccessKey: accessKey, + SecretKey: secretKey, Endpoint: endpoint, } + case "null": + return storage.NewNullStorage() default: - return defaultReplayStorage + return defaultStorage } } @@ -91,7 +146,9 @@ func NewCommandStorage() CommandStorage { docType = "command_store" } return storage.ESCommandStorage{Hosts: hosts, Index: index, DocType: docType} + case "null": + return storage.NewNullStorage() default: - return defaultCommandStorage + return defaultStorage } } diff --git a/jumpserver/koko/pkg/service/accesskey.go b/jumpserver/koko/pkg/service/accesskey.go index 5b7fd68b2f5d2e54457a187a65531e29fc52ee0f..d5d2fdaf5cf9e4225441cc0eab9aa5b9070e034c 100644 --- a/jumpserver/koko/pkg/service/accesskey.go +++ b/jumpserver/koko/pkg/service/accesskey.go @@ -70,10 +70,10 @@ func (ak *AccessKey) SaveToFile() error { } } f, err := os.Create(ak.Path) - defer f.Close() if err != nil { return err } + defer f.Close() _, err = f.WriteString(fmt.Sprintf("%s:%s", ak.ID, ak.Secret)) if err != nil { logger.Error(err) diff --git a/jumpserver/koko/pkg/service/cache.go b/jumpserver/koko/pkg/service/cache.go deleted file mode 100644 index 629eba1789ab22e78e6aa03fbe5a31f837ca00bb..0000000000000000000000000000000000000000 --- a/jumpserver/koko/pkg/service/cache.go +++ /dev/null @@ -1,71 +0,0 @@ -package service - -import ( - "sync" - - "github.com/jumpserver/koko/pkg/model" -) - -type assetsCacheContainer struct { - mapData map[string]model.AssetList - mapETag map[string]string - mu *sync.RWMutex -} - -func (c *assetsCacheContainer) Get(key string) (model.AssetList, bool) { - c.mu.RLock() - defer c.mu.RUnlock() - value, ok := c.mapData[key] - return value, ok -} - -func (c *assetsCacheContainer) GetETag(key string) (string, bool) { - c.mu.RLock() - defer c.mu.RUnlock() - value, ok := c.mapETag[key] - return value, ok -} - -func (c *assetsCacheContainer) SetValue(key string, value model.AssetList) { - c.mu.Lock() - defer c.mu.Unlock() - c.mapData[key] = value -} - -func (c *assetsCacheContainer) SetETag(key string, value string) { - c.mu.Lock() - defer c.mu.Unlock() - c.mapETag[key] = value -} - -type nodesCacheContainer struct { - mapData map[string]model.NodeList - mapETag map[string]string - mu *sync.RWMutex -} - -func (c *nodesCacheContainer) Get(key string) (model.NodeList, bool) { - c.mu.RLock() - defer c.mu.RUnlock() - value, ok := c.mapData[key] - return value, ok -} - -func (c *nodesCacheContainer) GetETag(key string) (string, bool) { - c.mu.RLock() - defer c.mu.RUnlock() - value, ok := c.mapETag[key] - return value, ok -} - -func (c *nodesCacheContainer) SetValue(key string, value model.NodeList) { - c.mu.Lock() - defer c.mu.Unlock() - c.mapData[key] = value -} - -func (c *nodesCacheContainer) SetETag(key string, value string) { - c.mu.Lock() - defer c.mu.Unlock() - c.mapETag[key] = value -} diff --git a/jumpserver/koko/pkg/service/database.go b/jumpserver/koko/pkg/service/database.go new file mode 100644 index 0000000000000000000000000000000000000000..66635e8b961d698c6ab69cfb9b2842471a9360f2 --- /dev/null +++ b/jumpserver/koko/pkg/service/database.go @@ -0,0 +1,45 @@ +package service + +import ( + "fmt" + + "github.com/jumpserver/koko/pkg/logger" + "github.com/jumpserver/koko/pkg/model" +) + +func GetUserDatabases(uid string) (res []model.Database) { + Url := fmt.Sprintf(DatabaseAPPURL, uid) + _, err := authClient.Get(Url, &res) + if err != nil { + logger.Errorf("Get User databases err: %s", err) + } + return +} + +func GetUserDatabaseSystemUsers(userID, assetID string) (sysUsers []model.SystemUser) { + Url := fmt.Sprintf(UserDatabaseSystemUsersURL, userID, assetID) + _, err := authClient.Get(Url, &sysUsers) + if err != nil { + logger.Error("Get user asset system users error: ", err) + } + return +} + +func GetSystemUserDatabaseAuthInfo(systemUserID string) (info model.SystemUserAuthInfo) { + Url := fmt.Sprintf(SystemUserAuthURL, systemUserID) + _, err := authClient.Get(Url, &info) + if err != nil { + logger.Errorf("Get system user %s auth info failed", systemUserID) + } + return +} + + +func GetDatabase(dbID string) (res model.Database) { + Url := fmt.Sprintf(DatabaseDetailURL, dbID) + _, err := authClient.Get(Url, &res) + if err != nil { + logger.Errorf("Get User databases err: %s", err) + } + return +} \ No newline at end of file diff --git a/jumpserver/koko/pkg/service/errorcode.go b/jumpserver/koko/pkg/service/errorcode.go new file mode 100644 index 0000000000000000000000000000000000000000..23cdccf074ec76fa2011f93f28799f451a0edbb7 --- /dev/null +++ b/jumpserver/koko/pkg/service/errorcode.go @@ -0,0 +1,9 @@ +package service + +const ( + ErrLoginConfirmWait = "login_confirm_wait" + ErrLoginConfirmRejected = "login_confirm_rejected" + ErrLoginConfirmRequired = "login_confirm_required" + ErrMFARequired = "mfa_required" + ErrPasswordFailed = "password_failed" +) diff --git a/jumpserver/koko/pkg/service/init.go b/jumpserver/koko/pkg/service/init.go index 3de263778c29a6c561854654bca39783349179ed..539a5787a1ff026f6a19ceec7490330215953f51 100644 --- a/jumpserver/koko/pkg/service/init.go +++ b/jumpserver/koko/pkg/service/init.go @@ -13,15 +13,12 @@ import ( "github.com/jumpserver/koko/pkg/logger" ) -var client = common.NewClient(30, "") var authClient = common.NewClient(30, "") func Initial(ctx context.Context) { cf := config.GetConf() keyPath := cf.AccessKeyFile - client.BaseHost = cf.CoreHost authClient.BaseHost = cf.CoreHost - client.SetHeader("X-JMS-ORG", "ROOT") authClient.SetHeader("X-JMS-ORG", "ROOT") if !path.IsAbs(cf.AccessKeyFile) { @@ -69,6 +66,7 @@ func MustLoadServerConfigOnce() { _, err := authClient.Get(TerminalConfigURL, &data) if err != nil { logger.Error("Load config from server error: ", err) + os.Exit(1) return } data["TERMINAL_HOST_KEY"] = "Hidden" diff --git a/jumpserver/koko/pkg/service/options.go b/jumpserver/koko/pkg/service/options.go new file mode 100644 index 0000000000000000000000000000000000000000..604855a9c5cb6a124de0f2de75a0203e5eb2b2b2 --- /dev/null +++ b/jumpserver/koko/pkg/service/options.go @@ -0,0 +1,50 @@ +package service + +type AuthStatus int64 + +const ( + AuthSuccess AuthStatus = iota + 1 + AuthFailed + AuthMFARequired + AuthConfirmRequired +) + +type SessionOption func(*SessionOptions) + +func Username(username string) SessionOption { + return func(args *SessionOptions) { + args.Username = username + } +} + +func Password(password string) SessionOption { + return func(args *SessionOptions) { + args.Password = password + } +} + +func PublicKey(publicKey string) SessionOption { + return func(args *SessionOptions) { + args.PublicKey = publicKey + } +} + +func RemoteAddr(remoteAddr string) SessionOption { + return func(args *SessionOptions) { + args.RemoteAddr = remoteAddr + } +} + +func LoginType(loginType string) SessionOption { + return func(args *SessionOptions) { + args.LoginType = loginType + } +} + +type SessionOptions struct { + Username string + Password string + PublicKey string + RemoteAddr string + LoginType string +} diff --git a/jumpserver/koko/pkg/service/perms.go b/jumpserver/koko/pkg/service/perms.go index 3bba29a8b0968fc744c98144ceceb7b6e0e98548..26b50c6894d8ce2593e66734dd3c734e248e21fa 100644 --- a/jumpserver/koko/pkg/service/perms.go +++ b/jumpserver/koko/pkg/service/perms.go @@ -9,24 +9,30 @@ import ( "github.com/jumpserver/koko/pkg/model" ) -func GetUserAssets(userID, search string, pageSize, offset int) (resp model.AssetsPaginationResponse) { +func GetUserAssets(userID string, pageSize, offset int, searches ...string) (resp model.AssetsPaginationResponse) { if pageSize < 0 { pageSize = 0 } + paramsArray := make([]map[string]string, 0, len(searches)+2) + for i := 0; i < len(searches); i++ { + paramsArray = append(paramsArray, map[string]string{ + "search": searches[i], + }) + } params := map[string]string{ - "search": url.QueryEscape(search), "limit": strconv.Itoa(pageSize), "offset": strconv.Itoa(offset), } - + paramsArray = append(paramsArray, params) Url := fmt.Sprintf(UserAssetsURL, userID) var err error if pageSize > 0 { - _, err = authClient.Get(Url, &resp, params) + _, err = authClient.Get(Url, &resp, paramsArray...) } else { var data model.AssetList - _, err = authClient.Get(Url, &data, params) + _, err = authClient.Get(Url, &data, paramsArray...) resp.Data = data + resp.Total = len(data) } if err != nil { logger.Error("Get user assets error: ", err) @@ -34,6 +40,21 @@ func GetUserAssets(userID, search string, pageSize, offset int) (resp model.Asse return } +func ForceRefreshUserPemAssets(userID string) error { + params := map[string]string{ + "limit": "1", + "offset": "0", + "cache": "2", + } + Url := fmt.Sprintf(UserAssetsURL, userID) + var resp model.AssetsPaginationResponse + _, err := authClient.Get(Url, &resp, params) + if err != nil { + logger.Errorf("Refresh user assets error: %s", err) + } + return err +} + func GetUserAllAssets(userID string) (assets []model.Asset) { Url := fmt.Sprintf(UserAssetsURL, userID) _, err := authClient.Get(Url, &assets) @@ -91,6 +112,38 @@ func GetUserNodeAssets(userID, nodeID, cachePolicy string) (assets model.AssetLi return } +func GetUserNodePaginationAssets(userID, nodeID string, pageSize, offset int, searches ...string) (resp model.AssetsPaginationResponse) { + if pageSize < 0 { + pageSize = 0 + } + paramsArray := make([]map[string]string, 0, len(searches)+2) + for i := 0; i < len(searches); i++ { + paramsArray = append(paramsArray, map[string]string{ + "search": url.QueryEscape(searches[i]), + }) + } + + params := map[string]string{ + "limit": strconv.Itoa(pageSize), + "offset": strconv.Itoa(offset), + } + paramsArray = append(paramsArray, params) + Url := fmt.Sprintf(UserNodeAssetsListURL, userID, nodeID) + var err error + if pageSize > 0 { + _, err = authClient.Get(Url, &resp, paramsArray...) + } else { + var data model.AssetList + _, err = authClient.Get(Url, &data, paramsArray...) + resp.Data = data + resp.Total = len(data) + } + if err != nil { + logger.Error("Get user node assets error: ", err) + } + return +} + func ValidateUserAssetPermission(userID, assetID, systemUserID, action string) bool { payload := map[string]string{ "user_id": userID, @@ -112,3 +165,50 @@ func ValidateUserAssetPermission(userID, assetID, systemUserID, action string) b return res.Msg } + +func ValidateUserDatabasePermission(userID, databaseID, systemUserID string) bool { + payload := map[string]string{ + "user_id": userID, + "database_app_id": databaseID, + "system_user_id": systemUserID, + } + Url := ValidateUserDatabasePermissionURL + var res struct { + Msg bool `json:"msg"` + } + _, err := authClient.Get(Url, &res, payload) + + if err != nil { + logger.Error(err) + return false + } + + return res.Msg +} + +func GetUserNodeTreeWithAsset(userID, nodeID, cachePolicy string) (nodeTrees model.NodeTreeList) { + if cachePolicy == "" { + cachePolicy = "1" + } + + payload := map[string]string{"cache_policy": cachePolicy} + if nodeID != "" { + payload["id"] = nodeID + } + Url := fmt.Sprintf(NodeTreeWithAssetURL, userID) + _, err := authClient.Get(Url, &nodeTrees, payload) + if err != nil { + logger.Error("Get user node tree error: ", err) + } + return +} + +func SearchPermAsset(uid, key string) (res model.NodeTreeList, err error) { + Url := fmt.Sprintf(UserAssetsTreeURL, uid) + payload := map[string]string{"search": key} + _, err = authClient.Get(Url, &res, payload) + if err != nil { + logger.Error("Get user node tree error: ", err) + } + return +} \ No newline at end of file diff --git a/jumpserver/koko/pkg/service/terminal.go b/jumpserver/koko/pkg/service/terminal.go index 6c955cf566af8210584f1c7dd5fc44f81b092e04..09a8eb92a7a6084a13c26e1f08851233044aedc9 100644 --- a/jumpserver/koko/pkg/service/terminal.go +++ b/jumpserver/koko/pkg/service/terminal.go @@ -8,9 +8,7 @@ import ( ) func RegisterTerminal(name, token, comment string) (res model.Terminal) { - if client.Headers == nil { - client.Headers = make(map[string]string) - } + client := newClient() client.Headers["Authorization"] = fmt.Sprintf("BootstrapToken %s", token) data := map[string]string{"name": name, "comment": comment} _, err := client.Post(TerminalRegisterURL, data, &res) diff --git a/jumpserver/koko/pkg/service/urls.go b/jumpserver/koko/pkg/service/urls.go index c2f75cfa33d3eb0da4c4fddfcf0967c1756621ac..764c14a32a4130f93770be91b7ef587705fdd3ac 100644 --- a/jumpserver/koko/pkg/service/urls.go +++ b/jumpserver/koko/pkg/service/urls.go @@ -1,11 +1,9 @@ package service const ( - UserAuthURL = "/api/v1/authentication/auth/" // post 验证用户登陆 UserProfileURL = "/api/v1/users/profile/" // 获取当前用户的基本信息 UserListURL = "/api/v1/users/users/" // 用户列表地址 UserDetailURL = "/api/v1/users/users/%s/" // 获取用户信息 - UserAuthOTPURL = "/api/v1/authentication/otp/auth/" // 验证OTP TokenAssetURL = "/api/v1/authentication/connection-token/?token=%s" // Token name SystemUserAssetAuthURL = "/api/v1/assets/system-users/%s/assets/%s/auth-info/" // 该系统用户对某资产的授权 @@ -38,3 +36,23 @@ const ( const ( UserAssetSystemUsersURL = "/api/v1/perms/users/%s/assets/%s/system-users/" // 获取用户授权资产的系统用户列表 ) + +// 1.5.5 +const ( + UserTokenAuthURL = "/api/v1/authentication/tokens/" // 用户登录验证 + UserConfirmAuthURL = "/api/v1/authentication/login-confirm-ticket/status/" + + NodeTreeWithAssetURL = "/api/v1/perms/users/%s/nodes/children-with-assets/tree/" // 资产树 + + DatabaseAPPURL = "/api/v1/perms/users/%s/database-apps/" //数据库app + + UserDatabaseSystemUsersURL = "/api/v1/perms/users/%s/database-apps/%s/system-users/" + + SystemUserAuthURL = "/api/v1/assets/system-users/%s/auth-info/" + + UserAssetsTreeURL = "/api/v1/perms/users/%s/assets/tree/" + + DatabaseDetailURL = "/api/v1/applications/database-apps/%s/" + + ValidateUserDatabasePermissionURL = "/api/v1/perms/database-app-permissions/user/validate/" +) diff --git a/jumpserver/koko/pkg/service/users.go b/jumpserver/koko/pkg/service/users.go index 9723c9e5689fae2d3ac7ba786b680d04afd22347..c879cbd37752e3c9a045d044f6266bed10804df6 100644 --- a/jumpserver/koko/pkg/service/users.go +++ b/jumpserver/koko/pkg/service/users.go @@ -1,32 +1,184 @@ package service import ( + "context" "fmt" + "time" "github.com/pkg/errors" + "github.com/jumpserver/koko/pkg/common" "github.com/jumpserver/koko/pkg/logger" "github.com/jumpserver/koko/pkg/model" ) -type AuthResp struct { - Token string `json:"token"` - Seed string `json:"seed"` - User *model.User `json:"user"` +type authResponse struct { + Err string `json:"error,omitempty"` + Msg string `json:"msg,omitempty"` + Data dataResponse `json:"data,omitempty"` + + Username string `json:"username,omitempty"` + Token string `json:"token,omitempty"` + Keyword string `json:"keyword,omitempty"` + DateExpired string `json:"date_expired,omitempty"` + + User model.User `json:"user,omitempty"` +} + +type dataResponse struct { + Choices []string `json:"choices,omitempty"` + Url string `json:"url,omitempty"` +} + +type AuthOptions struct { + Name string + Url string +} + +func NewSessionClient(setters ...SessionOption) SessionClient { + option := &SessionOptions{} + for _, setter := range setters { + setter(option) + } + conn := newClient() + conn.SetHeader("X-Forwarded-For", option.RemoteAddr) + conn.SetHeader("X-JMS-LOGIN-TYPE", option.LoginType) + return SessionClient{ + option: option, + client: &conn, + authOptions: make(map[string]AuthOptions), + } +} + +type SessionClient struct { + option *SessionOptions + client *common.Client + + authOptions map[string]AuthOptions +} + +func (u *SessionClient) SetOption(setters ...SessionOption) { + for _, setter := range setters { + setter(u.option) + } +} + +func (u *SessionClient) Authenticate(ctx context.Context) (user model.User, authStatus AuthStatus) { + authStatus = AuthFailed + data := map[string]string{ + "username": u.option.Username, + "password": u.option.Password, + "public_key": u.option.PublicKey, + "remote_addr": u.option.RemoteAddr, + "login_type": u.option.LoginType, + } + var resp authResponse + _, err := u.client.Post(UserTokenAuthURL, data, &resp) + if err != nil { + logger.Errorf("User %s Authenticate err: %s", u.option.Username, err) + return + } + if resp.Err != "" { + switch resp.Err { + case ErrLoginConfirmWait: + logger.Infof("User %s login need confirmation", u.option.Username) + authStatus = AuthConfirmRequired + case ErrMFARequired: + for _, item := range resp.Data.Choices { + u.authOptions[item] = AuthOptions{ + Name: item, + Url: resp.Data.Url, + } + } + logger.Infof("User %s login need MFA", u.option.Username) + authStatus = AuthMFARequired + default: + logger.Errorf("User %s login err: %s", u.option.Username, resp.Err) + } + return + } + if resp.Token != "" { + return resp.User, AuthSuccess + } + return } -func Authenticate(username, password, publicKey, remoteAddr, loginType string) (resp *AuthResp, err error) { +func (u *SessionClient) CheckUserOTP(ctx context.Context, code string) (user model.User, authStatus AuthStatus) { + var err error + authStatus = AuthFailed data := map[string]string{ - "username": username, - "password": password, - "public_key": publicKey, - "remote_addr": remoteAddr, - "login_type": loginType, + "code": code, + "remote_addr": u.option.RemoteAddr, + "login_type": u.option.LoginType, + } + for name, authData := range u.authOptions { + var resp authResponse + switch name { + case "opt": + data["type"] = name + } + _, err = u.client.Post(authData.Url, data, &resp) + if err != nil { + logger.Errorf("User %s use %s check MFA err: %s", u.option.Username, name, err) + continue + } + if resp.Err != "" { + logger.Errorf("User %s use %s check MFA err: %s", u.option.Username, name, resp.Err) + continue + } + if resp.Msg == "ok" { + logger.Infof("User %s check MFA success, check if need admin confirm", u.option.Username) + return u.Authenticate(ctx) + } } - _, err = client.Post(UserAuthURL, data, &resp) + logger.Errorf("User %s failed to check MFA", u.option.Username) return } +func (u *SessionClient) CheckConfirm(ctx context.Context) (user model.User, authStatus AuthStatus) { + var err error + for { + select { + case <-ctx.Done(): + logger.Errorf("User %s exit and cancel confirmation", u.option.Username) + u.CancelConfirm() + return + case <-time.After(5 * time.Second): + var resp authResponse + _, err = u.client.Get(UserConfirmAuthURL, &resp) + if err != nil { + logger.Errorf("User %s check confirm err: %s", u.option.Username, err) + return + } + if resp.Err != "" { + switch resp.Err { + case ErrLoginConfirmWait: + logger.Infof("User %s still wait confirm", u.option.Username) + continue + case ErrLoginConfirmRejected: + logger.Infof("User %s confirmation was rejected by admin", u.option.Username) + default: + logger.Infof("User %s confirmation was rejected by err: %s", u.option.Username, resp.Err) + } + return + } + if resp.Msg == "ok" { + logger.Infof("User %s confirmation was accepted", u.option.Username) + return u.Authenticate(ctx) + } + } + } +} + +func (u *SessionClient) CancelConfirm() { + _, err := u.client.Delete(UserConfirmAuthURL, nil) + if err != nil { + logger.Errorf("Cancel User %s confirmation err: %s", u.option.Username, err) + return + } + logger.Infof("Cancel User %s confirmation success", u.option.Username) +} + func GetUserDetail(userID string) (user *model.User) { Url := fmt.Sprintf(UserDetailURL, userID) _, err := authClient.Get(Url, &user) @@ -56,20 +208,6 @@ func GetUserByUsername(username string) (user *model.User, err error) { return } -func CheckUserOTP(seed, code, remoteAddr, loginType string) (resp *AuthResp, err error) { - data := map[string]string{ - "seed": seed, - "otp_code": code, - "remote_addr": remoteAddr, - "login_type": loginType, - } - _, err = client.Post(UserAuthOTPURL, data, &resp) - if err != nil { - return - } - return -} - func CheckUserCookie(sessionID, csrfToken string) (user *model.User, err error) { cli := newClient() cli.SetCookie("csrftoken", csrfToken) diff --git a/jumpserver/koko/pkg/srvconn/connmanager.go b/jumpserver/koko/pkg/srvconn/connmanager.go index 277f1a5f7ad9ce74a5ef1c9ccec771b209407eac..80f7ddae666a3b639dc3a6369928bade26fed61b 100644 --- a/jumpserver/koko/pkg/srvconn/connmanager.go +++ b/jumpserver/koko/pkg/srvconn/connmanager.go @@ -29,6 +29,20 @@ var ( "arcfour256", "arcfour128", "arcfour", "aes128-cbc", "3des-cbc"} + + supportedKexAlgos = []string{ + "diffie-hellman-group1-sha1", + "diffie-hellman-group14-sha1", "ecdh-sha2-nistp256", "ecdh-sha2-nistp521", + "ecdh-sha2-nistp384", "curve25519-sha256@libssh.org"} + + supportedHostKeyAlgos = []string{ + "ssh-rsa-cert-v01@openssh.com", "ssh-dss-cert-v01@openssh.com", "ecdsa-sha2-nistp256-cert-v01@openssh.com", + "ecdsa-sha2-nistp384-cert-v01@openssh.com", "ecdsa-sha2-nistp521-cert-v01@openssh.com", + "ssh-ed25519-cert-v01@openssh.com", + "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521", + "ssh-rsa", "ssh-dss", + "ssh-ed25519", + } ) type SSHClient struct { @@ -141,6 +155,7 @@ func (sc *SSHClientConfig) Config() (config *gossh.ClientConfig, err error) { authMethods := make([]gossh.AuthMethod, 0) if sc.Password != "" { authMethods = append(authMethods, gossh.Password(sc.Password)) + authMethods = append(authMethods, gossh.KeyboardInteractive(sc.keyboardInteractivePassword(sc.Password))) } if sc.PrivateKeyPath != "" { if pubkey, err := GetPubKeyFromFile(sc.PrivateKeyPath); err != nil { @@ -159,15 +174,26 @@ func (sc *SSHClientConfig) Config() (config *gossh.ClientConfig, err error) { } } config = &gossh.ClientConfig{ - User: sc.User, - Auth: authMethods, - HostKeyCallback: gossh.InsecureIgnoreHostKey(), - Config: gossh.Config{Ciphers: supportedCiphers}, - Timeout: sc.Timeout, + User: sc.User, + Auth: authMethods, + HostKeyCallback: gossh.InsecureIgnoreHostKey(), + Config: gossh.Config{Ciphers: supportedCiphers, KeyExchanges: supportedKexAlgos}, + Timeout: sc.Timeout, + HostKeyAlgorithms: supportedHostKeyAlgos, } return config, nil } +func (sc *SSHClientConfig) keyboardInteractivePassword(password string) gossh.KeyboardInteractiveChallenge { + return func(user, instruction string, questions []string, echos []bool) (answers []string, err error) { + if len(questions) == 0 { + return []string{}, nil + } + logger.Infof("Host %s use keyboard-Interactive auth method login", sc.Host) + return []string{password}, nil + } +} + func (sc *SSHClientConfig) DialProxy() (client *gossh.Client, err error) { for _, p := range sc.Proxy { client, err = p.Dial() diff --git a/jumpserver/koko/pkg/srvconn/mysqlconn.go b/jumpserver/koko/pkg/srvconn/mysqlconn.go new file mode 100644 index 0000000000000000000000000000000000000000..9b6197650f03c37361ff557839104aef1b2461fc --- /dev/null +++ b/jumpserver/koko/pkg/srvconn/mysqlconn.go @@ -0,0 +1,194 @@ +package srvconn + +import ( + "bytes" + "errors" + "fmt" + "os" + "os/exec" + "os/user" + "strconv" + "sync" + "syscall" + "time" + + "github.com/creack/pty" + + "github.com/jumpserver/koko/pkg/logger" +) + +const ( + mysqlPrompt = "Enter password: " +) + +func NewMysqlServer(ops ...SqlOption) *ServerMysqlConnection { + args := &SqlOptions{ + Username: os.Getenv("USER"), + Password: os.Getenv("PASSWORD"), + Host: "127.0.0.1", + Port: 3306, + DBName: "", + } + for _, setter := range ops { + setter(args) + } + return &ServerMysqlConnection{options: args, onceClose: new(sync.Once)} +} + +type ServerMysqlConnection struct { + options *SqlOptions + ptyFD *os.File + onceClose *sync.Once + cmd *exec.Cmd +} + +func (dbconn *ServerMysqlConnection) Connect() (err error) { + cmd := exec.Command("mysql", dbconn.options.CommandArgs()...) + nobody, err := user.Lookup("nobody") + if err != nil { + logger.Errorf("lookup nobody user err: %s", err) + return errors.New("nobody user does not exist") + } + cmd.SysProcAttr = &syscall.SysProcAttr{} + uid, _ := strconv.Atoi(nobody.Uid) + gid, _ := strconv.Atoi(nobody.Gid) + cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)} + ptyFD, err := pty.Start(cmd) + go func() { + err = cmd.Wait() + if err != nil { + logger.Errorf("mysql command exit err: %s", err) + } + if ptyFD != nil { + _ = ptyFD.Close() + } + logger.Info("mysql connect closed.") + var wstatus syscall.WaitStatus + _, err = syscall.Wait4(-1, &wstatus, 0, nil) + }() + if err != nil { + logger.Errorf("pty start err: %s", err) + return fmt.Errorf("start local pty err: %s", err) + } + prompt := [len(mysqlPrompt)]byte{} + nr, err := ptyFD.Read(prompt[:]) + if err != nil { + _ = ptyFD.Close() + _ = cmd.Process.Kill() + logger.Errorf("read mysql pty local fd err: %s", err) + return fmt.Errorf("mysql conn err: %s", err) + } + if !bytes.Equal(prompt[:nr], []byte(mysqlPrompt)) { + _ = cmd.Process.Kill() + _ = ptyFD.Close() + logger.Errorf("mysql login prompt characters did not match: %s", prompt[:nr]) + return errors.New("failed login mysql") + } + // 输入密码, 登录mysql + _, err = ptyFD.Write([]byte(dbconn.options.Password + "\r\n")) + if err != nil { + _ = ptyFD.Close() + _ = cmd.Process.Kill() + logger.Errorf("mysql local pty write err: %s", err) + return fmt.Errorf("mysql conn err: %s", err) + } + logger.Infof("Connect mysql database %s success ", dbconn.options.Host) + dbconn.cmd = cmd + dbconn.ptyFD = ptyFD + return +} + +func (dbconn *ServerMysqlConnection) Read(p []byte) (int, error) { + if dbconn.ptyFD == nil { + return 0, fmt.Errorf("not connect init") + } + return dbconn.ptyFD.Read(p) +} + +func (dbconn *ServerMysqlConnection) Write(p []byte) (int, error) { + if dbconn.ptyFD == nil { + return 0, fmt.Errorf("not connect init") + } + return dbconn.ptyFD.Write(p) +} + +func (dbconn *ServerMysqlConnection) SetWinSize(h, w int) error { + if dbconn.ptyFD == nil { + return fmt.Errorf("not connect init") + } + win := pty.Winsize{ + Rows: uint16(h), + Cols: uint16(w), + } + logger.Infof("db conn windows size change %d*%d", h, w) + return pty.Setsize(dbconn.ptyFD, &win) +} + +func (dbconn *ServerMysqlConnection) Close() (err error) { + dbconn.onceClose.Do(func() { + if dbconn.ptyFD == nil { + return + } + _ = dbconn.ptyFD.Close() + err = dbconn.cmd.Process.Signal(os.Kill) + }) + return +} + +func (dbconn *ServerMysqlConnection) Timeout() time.Duration { + return time.Duration(10) * time.Second +} + +func (dbconn *ServerMysqlConnection) Protocol() string { + return "mysql" +} + +type SqlOptions struct { + Username string + Password string + DBName string + Host string + Port int +} + +func (opts *SqlOptions) CommandArgs() []string { + return []string{ + fmt.Sprintf("--user=%s", opts.Username), + fmt.Sprintf("--host=%s", opts.Host), + fmt.Sprintf("--port=%d", opts.Port), + "--password", + opts.DBName, + } +} + +type SqlOption func(*SqlOptions) + +func SqlUsername(username string) SqlOption { + return func(args *SqlOptions) { + args.Username = username + } +} + +func SqlPassword(password string) SqlOption { + return func(args *SqlOptions) { + args.Password = password + } +} + +func SqlDBName(dbName string) SqlOption { + return func(args *SqlOptions) { + args.DBName = dbName + } +} + +func SqlHost(host string) SqlOption { + return func(args *SqlOptions) { + args.Host = host + } +} + +func SqlPort(port int) SqlOption { + return func(args *SqlOptions) { + args.Port = port + } +} diff --git a/jumpserver/koko/pkg/srvconn/sftpconn.go b/jumpserver/koko/pkg/srvconn/sftpconn.go index 1d83f9becc89eca14de9e66331e8ad29f445f923..094f027e5168700abfa24f57a25e5c94a7fa7bb8 100644 --- a/jumpserver/koko/pkg/srvconn/sftpconn.go +++ b/jumpserver/koko/pkg/srvconn/sftpconn.go @@ -1,763 +1,464 @@ package srvconn import ( + "encoding/json" "fmt" "os" - "path/filepath" - "sort" "strings" "syscall" "time" "github.com/pkg/sftp" - "github.com/jumpserver/koko/pkg/common" - "github.com/jumpserver/koko/pkg/config" "github.com/jumpserver/koko/pkg/logger" "github.com/jumpserver/koko/pkg/model" "github.com/jumpserver/koko/pkg/service" ) -func NewUserSFTP(user *model.User, addr string, assets ...model.Asset) *UserSftp { - u := UserSftp{ - User: user, Addr: addr, - } - u.initial(assets) - return &u -} - -type UserSftp struct { +type UserSftpConn struct { User *model.User Addr string + Dirs map[string]os.FileInfo - RootPath string - ShowHidden bool - ReuseConnection bool - Overtime time.Duration - hosts map[string]*HostnameDir // key hostname or hostname.orgName - sftpClients map[string]*SftpConn // key %s@%s suName hostName + modeTime time.Time + logChan chan *model.FTPLog - LogChan chan *model.FTPLog -} + closed chan struct{} -func (u *UserSftp) initial(assets []model.Asset) { - conf := config.GetConf() - u.RootPath = conf.SftpRoot - u.ShowHidden = conf.ShowHiddenFile - u.ReuseConnection = conf.ReuseConnection - u.Overtime = conf.SSHTimeout * time.Second - u.hosts = make(map[string]*HostnameDir) - u.sftpClients = make(map[string]*SftpConn) - u.LogChan = make(chan *model.FTPLog, 10) - for i := 0; i < len(assets); i++ { - if !assets[i].IsSupportProtocol("ssh") { - continue - } - key := assets[i].Hostname - if assets[i].OrgID != "" { - key = fmt.Sprintf("%s.%s", assets[i].Hostname, assets[i].OrgName) - } - u.hosts[key] = NewHostnameDir(&assets[i]) - } - - go u.LoopPushFTPLog() + searchDir *SearchResultDir } -func (u *UserSftp) ReadDir(path string) (res []os.FileInfo, err error) { - req := u.ParsePath(path) - if req.host == "" { - return u.RootDirInfo() - } - - host, ok := u.hosts[req.host] - if !ok { - return res, sftp.ErrSshFxNoSuchFile +func (u *UserSftpConn) ReadDir(path string) (res []os.FileInfo, err error) { + fi, restPath := u.ParsePath(path) + if rootDir, ok := fi.(*UserSftpConn); ok { + return rootDir.List() } - if req.su == "" { - for _, su := range host.GetSystemUsers() { - res = append(res, NewFakeFile(su.Name, true)) - } - return - } - host.loadSystemUsers(u.User.ID) - su, ok := host.suMaps[req.su] - if !ok { - return res, sftp.ErrSshFxNoSuchFile + if nodeDir, ok := fi.(*NodeDir); ok { + return nodeDir.List() } - if !u.validatePermission(su, model.ConnectAction) { - return res, sftp.ErrSshFxPermissionDenied + if assetDir, ok := fi.(*AssetDir); ok { + return assetDir.ReadDir(restPath) } - conn, realPath := u.GetSFTPAndRealPath(req) - if conn == nil { - return res, sftp.ErrSshFxPermissionDenied - } - logger.Debug("inter sftp read dir real path: ", realPath) - res, err = conn.client.ReadDir(realPath) - if !u.ShowHidden { - noHiddenFiles := make([]os.FileInfo, 0, len(res)) - for i := 0; i < len(res); i++ { - if !strings.HasPrefix(res[i].Name(), ".") { - noHiddenFiles = append(noHiddenFiles, res[i]) - } - } - return noHiddenFiles, err - } - return res, err + return nil, sftp.ErrSSHFxNoSuchFile } -func (u *UserSftp) Stat(path string) (res os.FileInfo, err error) { - req := u.ParsePath(path) - if req.host == "" { - return u.Info() - } - host, ok := u.hosts[req.host] - if !ok { - return res, sftp.ErrSshFxNoSuchFile +func (u *UserSftpConn) Stat(path string) (res os.FileInfo, err error) { + fi, restPath := u.ParsePath(path) + if rootDir, ok := fi.(*UserSftpConn); ok { + return rootDir, nil } - if req.su == "" { - res = NewFakeFile(req.host, true) - return - } - host.loadSystemUsers(u.User.ID) - su, ok := host.suMaps[req.su] - if !ok { - return res, sftp.ErrSshFxNoSuchFile + if nodeDir, ok := fi.(*NodeDir); ok { + return nodeDir, nil } - if !u.validatePermission(su, model.ConnectAction) { - return res, sftp.ErrSshFxPermissionDenied + if assetDir, ok := fi.(*AssetDir); ok { + return assetDir.Stat(restPath) } - conn, realPath := u.GetSFTPAndRealPath(req) - if conn == nil { - return res, sftp.ErrSshFxPermissionDenied - } - return conn.client.Stat(realPath) + return nil, sftp.ErrSSHFxNoSuchFile } -func (u *UserSftp) ReadLink(path string) (res string, err error) { - req := u.ParsePath(path) - if req.host == "" { - return res, sftp.ErrSshFxPermissionDenied - } - host, ok := u.hosts[req.host] - if !ok { - return res, sftp.ErrSshFxPermissionDenied - } - - if req.su == "" { - return res, sftp.ErrSshFxPermissionDenied - } - host.loadSystemUsers(u.User.ID) - su, ok := host.suMaps[req.su] - if !ok { - return res, sftp.ErrSshFxNoSuchFile - } - if !u.validatePermission(su, model.ConnectAction) { - return res, sftp.ErrSshFxPermissionDenied - } - conn, realPath := u.GetSFTPAndRealPath(req) - if conn == nil { - return res, sftp.ErrSshFxPermissionDenied +func (u *UserSftpConn) ReadLink(path string) (name string, err error) { + fi, restPath := u.ParsePath(path) + if _, ok := fi.(*UserSftpConn); ok && restPath == "" { + return "", sftp.ErrSshFxOpUnsupported } - return conn.client.ReadLink(realPath) -} -func (u *UserSftp) RemoveDirectory(path string) error { - req := u.ParsePath(path) - if req.host == "" { - return sftp.ErrSshFxPermissionDenied - } - host, ok := u.hosts[req.host] - if !ok { - return sftp.ErrSshFxPermissionDenied + if _, ok := fi.(*NodeDir); ok { + return "", sftp.ErrSshFxOpUnsupported } - if req.su == "" { - return sftp.ErrSshFxPermissionDenied - } - host.loadSystemUsers(u.User.ID) - su, ok := host.suMaps[req.su] - if !ok { - return sftp.ErrSshFxNoSuchFile + if assetDir, ok := fi.(*AssetDir); ok { + return assetDir.ReadLink(restPath) } - if !u.validatePermission(su, model.UploadAction) { - return sftp.ErrSshFxPermissionDenied - } - - conn, realPath := u.GetSFTPAndRealPath(req) - if conn == nil { - return sftp.ErrSshFxPermissionDenied - } - err := u.removeDirectoryAll(conn.client, realPath) - filename := realPath - isSuccess := false - operate := model.OperateRemoveDir - if err == nil { - isSuccess = true - } - u.CreateFTPLog(host.asset, su, operate, filename, isSuccess) - return err + return "", sftp.ErrSshFxOpUnsupported } -func (u *UserSftp) removeDirectoryAll(conn *sftp.Client, path string) error { - var err error - var files []os.FileInfo - files, err = conn.ReadDir(path) - if err != nil { - return err - } - for _, item := range files { - realPath := filepath.Join(path, item.Name()) - - if item.IsDir() { - err = u.removeDirectoryAll(conn, realPath) - if err != nil { - return err +func (u *UserSftpConn) Rename(oldNamePath, newNamePath string) (err error) { + oldFi, oldRestPath := u.ParsePath(oldNamePath) + newFi, newRestPath := u.ParsePath(newNamePath) + if oldAssetDir, ok := oldFi.(*AssetDir); ok { + if newAssetDir, newOk := newFi.(*AssetDir); newOk { + if oldAssetDir == newAssetDir { + return oldAssetDir.Rename(oldRestPath, newRestPath) } - continue - } - err = conn.Remove(realPath) - if err != nil { - return err } + } - return conn.RemoveDirectory(path) + return sftp.ErrSshFxOpUnsupported } -func (u *UserSftp) Remove(path string) error { - req := u.ParsePath(path) - if req.host == "" { - return sftp.ErrSshFxPermissionDenied - } - host, ok := u.hosts[req.host] - if !ok { - return sftp.ErrSshFxPermissionDenied - } - if req.su == "" { - return sftp.ErrSshFxPermissionDenied - } - host.loadSystemUsers(u.User.ID) - su, ok := host.suMaps[req.su] - if !ok { - return sftp.ErrSshFxNoSuchFile - } - if !u.validatePermission(su, model.UploadAction) { +func (u *UserSftpConn) RemoveDirectory(path string) (err error) { + fi, restPath := u.ParsePath(path) + if _, ok := fi.(*UserSftpConn); ok && restPath == "" { return sftp.ErrSshFxPermissionDenied } - conn, realPath := u.GetSFTPAndRealPath(req) - if conn == nil { + if _, ok := fi.(*NodeDir); ok { return sftp.ErrSshFxPermissionDenied } - logger.Debug("remove file path", realPath) - err := conn.client.Remove(realPath) - filename := realPath - isSuccess := false - operate := model.OperateDelete - if err == nil { - isSuccess = true + if assetDir, ok := fi.(*AssetDir); ok { + return assetDir.RemoveDirectory(restPath) } - u.CreateFTPLog(host.asset, su, operate, filename, isSuccess) - return err + return sftp.ErrSshFxPermissionDenied } -func (u *UserSftp) MkdirAll(path string) error { - req := u.ParsePath(path) - if req.host == "" { - return sftp.ErrSshFxPermissionDenied - } - host, ok := u.hosts[req.host] - if !ok { - return sftp.ErrSshFxPermissionDenied - } - if req.su == "" { - return sftp.ErrSshFxPermissionDenied - } - host.loadSystemUsers(u.User.ID) - su, ok := host.suMaps[req.su] - if !ok { - return sftp.ErrSshFxNoSuchFile - } - if !u.validatePermission(su, model.UploadAction) { +func (u *UserSftpConn) Remove(path string) (err error) { + fi, restPath := u.ParsePath(path) + if _, ok := fi.(*UserSftpConn); ok && restPath == "" { return sftp.ErrSshFxPermissionDenied } - conn, realPath := u.GetSFTPAndRealPath(req) - if conn == nil { + if _, ok := fi.(*NodeDir); ok { return sftp.ErrSshFxPermissionDenied } - err := conn.client.MkdirAll(realPath) - - filename := realPath - isSuccess := false - operate := model.OperateMkdir - if err == nil { - isSuccess = true + if assetDir, ok := fi.(*AssetDir); ok { + return assetDir.Remove(restPath) } - u.CreateFTPLog(host.asset, su, operate, filename, isSuccess) - return err + return sftp.ErrSshFxPermissionDenied } -func (u *UserSftp) Rename(oldNamePath, newNamePath string) error { - req1 := u.ParsePath(oldNamePath) - req2 := u.ParsePath(newNamePath) - if req1.host == "" || req2.host == "" || req1.su == "" || req2.su == "" { - return sftp.ErrSshFxPermissionDenied - } else if req1.host != req2.host || req1.su != req2.su { +func (u *UserSftpConn) MkdirAll(path string) (err error) { + fi, restPath := u.ParsePath(path) + if _, ok := fi.(*UserSftpConn); ok && restPath == "" { return sftp.ErrSshFxPermissionDenied } - host, ok := u.hosts[req1.host] - if !ok { - return sftp.ErrSshFxPermissionDenied - } - host.loadSystemUsers(u.User.ID) - su, ok := host.suMaps[req1.su] - if !ok { - return sftp.ErrSshFxNoSuchFile - } - if !u.validatePermission(su, model.UploadAction) { - return sftp.ErrSshFxPermissionDenied - } - - conn1, oldRealPath := u.GetSFTPAndRealPath(req1) - conn2, newRealPath := u.GetSFTPAndRealPath(req2) - if conn1 != conn2 { - return sftp.ErrSshFxOpUnsupported - } - err := conn1.client.Rename(oldRealPath, newRealPath) - filename := fmt.Sprintf("%s=>%s", oldRealPath, newRealPath) - isSuccess := false - operate := model.OperateRename - if err == nil { - isSuccess = true - } - u.CreateFTPLog(host.asset, su, operate, filename, isSuccess) - return err -} - -func (u *UserSftp) Symlink(oldNamePath, newNamePath string) error { - req1 := u.ParsePath(oldNamePath) - req2 := u.ParsePath(newNamePath) - if req1.host == "" || req2.host == "" || req1.su == "" || req2.su == "" { - return sftp.ErrSshFxPermissionDenied - } else if req1.host != req2.host || req1.su != req2.su { - return sftp.ErrSshFxPermissionDenied - } - host, ok := u.hosts[req1.host] - if !ok { - return sftp.ErrSshFxPermissionDenied - } - host.loadSystemUsers(u.User.ID) - su, ok := host.suMaps[req1.su] - if !ok { - return sftp.ErrSshFxNoSuchFile - } - if !u.validatePermission(su, model.UploadAction) { + if _, ok := fi.(*NodeDir); ok { return sftp.ErrSshFxPermissionDenied } - - conn1, oldRealPath := u.GetSFTPAndRealPath(req1) - conn2, newRealPath := u.GetSFTPAndRealPath(req2) - if conn1 != conn2 { - return sftp.ErrSshFxOpUnsupported + if assetDir, ok := fi.(*AssetDir); ok { + return assetDir.MkdirAll(restPath) } - err := conn1.client.Symlink(oldRealPath, newRealPath) + return sftp.ErrSshFxPermissionDenied +} - filename := fmt.Sprintf("%s=>%s", oldRealPath, newRealPath) - isSuccess := false - operate := model.OperateSymlink - if err == nil { - isSuccess = true +func (u *UserSftpConn) Symlink(oldNamePath, newNamePath string) (err error) { + oldFi, oldRestPath := u.ParsePath(oldNamePath) + newFi, newRestPath := u.ParsePath(newNamePath) + if oldAssetDir, ok := oldFi.(*AssetDir); ok { + if newAssetDir, newOk := newFi.(*AssetDir); newOk { + if oldAssetDir == newAssetDir { + return oldAssetDir.Symlink(oldRestPath, newRestPath) + } + } } - u.CreateFTPLog(host.asset, su, operate, filename, isSuccess) - return err + return sftp.ErrSshFxPermissionDenied } -func (u *UserSftp) Create(path string) (*sftp.File, error) { - req := u.ParsePath(path) - if req.host == "" { - return nil, sftp.ErrSshFxPermissionDenied - } - host, ok := u.hosts[req.host] - if !ok { - return nil, sftp.ErrSshFxPermissionDenied - } - if req.su == "" { - return nil, sftp.ErrSshFxPermissionDenied - } - host.loadSystemUsers(u.User.ID) - su, ok := host.suMaps[req.su] - if !ok { - return nil, sftp.ErrSshFxNoSuchFile - } - if !u.validatePermission(su, model.UploadAction) { +func (u *UserSftpConn) Create(path string) (*sftp.File, error) { + fi, restPath := u.ParsePath(path) + if _, ok := fi.(*UserSftpConn); ok { return nil, sftp.ErrSshFxPermissionDenied } - conn, realPath := u.GetSFTPAndRealPath(req) - if conn == nil { + if _, ok := fi.(*NodeDir); ok { return nil, sftp.ErrSshFxPermissionDenied } - logger.Debug("create path:", realPath) - sf, err := conn.client.Create(realPath) - filename := realPath - isSuccess := false - operate := model.OperateUpload - if err == nil { - isSuccess = true + if assetDir, ok := fi.(*AssetDir); ok { + return assetDir.Create(restPath) } - u.CreateFTPLog(host.asset, su, operate, filename, isSuccess) - return sf, err + + return nil, sftp.ErrSshFxPermissionDenied } -func (u *UserSftp) Open(path string) (*sftp.File, error) { - req := u.ParsePath(path) - if req.host == "" { +func (u *UserSftpConn) Open(path string) (*sftp.File, error) { + fi, restPath := u.ParsePath(path) + if _, ok := fi.(*UserSftpConn); ok { return nil, sftp.ErrSshFxPermissionDenied } - host, ok := u.hosts[req.host] - if !ok { - return nil, sftp.ErrSshFxPermissionDenied - } - if req.su == "" { + + if _, ok := fi.(*NodeDir); ok { return nil, sftp.ErrSshFxPermissionDenied } - host.loadSystemUsers(u.User.ID) - su, ok := host.suMaps[req.su] - if !ok { - return nil, sftp.ErrSshFxNoSuchFile - } - if !u.validatePermission(su, model.DownloadAction) { - return nil, sftp.ErrSshFxPermissionDenied + if assetDir, ok := fi.(*AssetDir); ok { + return assetDir.Open(restPath) } - conn, realPath := u.GetSFTPAndRealPath(req) - if conn == nil { - return nil, sftp.ErrSshFxPermissionDenied + + return nil, sftp.ErrSshFxPermissionDenied +} + +func (u *UserSftpConn) Close() { + for _, dir := range u.Dirs { + if nodeDir, ok := dir.(*NodeDir); ok { + nodeDir.close() + continue + } + if assetDir, ok := dir.(*AssetDir); ok { + assetDir.close() + continue + } } - logger.Debug("Open path:", realPath) - sf, err := conn.client.Open(realPath) - filename := realPath - isSuccess := false - operate := model.OperateDownaload - if err == nil { - isSuccess = true + if u.searchDir != nil{ + u.searchDir.close() } - u.CreateFTPLog(host.asset, su, operate, filename, isSuccess) - return sf, err + close(u.closed) +} + +func (u *UserSftpConn) Name() string { + return "/" } -func (u *UserSftp) Info() (os.FileInfo, error) { - return NewFakeFile("/", true), nil +func (u *UserSftpConn) Size() int64 { return 0 } + +func (u *UserSftpConn) Mode() os.FileMode { + return os.ModePerm | os.ModeDir } -func (u *UserSftp) RootDirInfo() ([]os.FileInfo, error) { - hostDirs := make([]os.FileInfo, 0, len(u.hosts)) - for key := range u.hosts { - hostDirs = append(hostDirs, NewFakeFile(key, true)) +func (u *UserSftpConn) ModTime() time.Time { return u.modeTime } + +func (u *UserSftpConn) IsDir() bool { return true } + +func (u *UserSftpConn) Sys() interface{} { + return &syscall.Stat_t{Uid: 0, Gid: 0} +} + +func (u *UserSftpConn) List() (res []os.FileInfo, err error) { + for _, item := range u.Dirs { + res = append(res, item) } - sort.Sort(FileInfoList(hostDirs)) - return hostDirs, nil + return } -func (u *UserSftp) ParsePath(path string) (req requestMessage) { - data := strings.Split(strings.TrimPrefix(path, "/"), "/") - if len(data) == 0 { +func (u *UserSftpConn) ParsePath(path string) (fi os.FileInfo, restPath string) { + path = strings.TrimPrefix(path, "/") + data := strings.Split(path, "/") + if len(data) == 1 && data[0] == "" { + fi = u return } - host, pathArray := data[0], data[1:] - req.host = host - if suName, unique := u.HostHasUniqueSu(host); unique { - req.suUnique = true - req.su = suName + var dirs map[string]os.FileInfo + var ok bool + + if data[0] == SearchFolderName { + dirs = u.searchDir.subDirs + data = data[1:] } else { - if len(pathArray) == 0 { - req.su = "" - } else { - req.su, pathArray = pathArray[0], pathArray[1:] + dirs = u.Dirs + } + for i := 0; i < len(data); i++ { + fi, ok = dirs[data[i]] + if !ok { + restPath = strings.Join(data[i+1:], "/") + break + } + if nodeDir, ok := fi.(*NodeDir); ok { + nodeDir.loadNodeAsset(u) + dirs = nodeDir.subDirs + continue + } + if assetDir, ok := fi.(*AssetDir); ok { + assetDir.loadSystemUsers() + restPath = strings.Join(data[i+1:], "/") + break } } - req.dpath = strings.Join(pathArray, "/") return } -func (u *UserSftp) GetSFTPAndRealPath(req requestMessage) (conn *SftpConn, realPath string) { - if host, ok := u.hosts[req.host]; ok { - if su, ok := host.suMaps[req.su]; ok { - key := fmt.Sprintf("%s@%s", su.Name, req.host) - conn, ok := u.sftpClients[key] - if !ok { - var err error - conn, err = u.GetSftpClient(host.asset, su) - if err != nil { - logger.Info("Get Sftp Client err: ", err.Error()) - return nil, "" +func (u *UserSftpConn) initial() { + nodeTrees := service.GetUserNodeTreeWithAsset(u.User.ID, "", "") + if u.Dirs == nil { + u.Dirs = map[string]os.FileInfo{} + } + u.searchDir = &SearchResultDir{ + folderName: SearchFolderName, + modeTime: time.Now().UTC(), + subDirs: map[string]os.FileInfo{}} + + for _, item := range nodeTrees { + if item.Pid != "" { + continue + } + typeName, ok := item.Meta["type"].(string) + if !ok { + continue + } + body, err := json.Marshal(item.Meta[typeName]) + if err != nil { + logger.Errorf("Json Marshal err: %s", err) + continue + } + switch typeName { + case "node": + node, err := model.ConvertMetaToNode(body) + if err != nil { + logger.Errorf("convert to node err: %s", err) + continue + } + nodeDir := NewNodeDir(node) + folderName := nodeDir.folderName + for { + _, ok := u.Dirs[folderName] + if !ok { + break } - u.sftpClients[key] = conn + folderName = fmt.Sprintf("%s_", folderName) } - - switch strings.ToLower(u.RootPath) { - case "home", "~", "": - realPath = filepath.Join(conn.HomeDirPath, strings.TrimPrefix(req.dpath, "/")) - default: - realPath = filepath.Join(u.RootPath, strings.TrimPrefix(req.dpath, "/")) + if folderName != nodeDir.folderName { + nodeDir.folderName = folderName } - return conn, realPath - } - } - return -} -func (u *UserSftp) HostHasUniqueSu(hostKey string) (string, bool) { - if host, ok := u.hosts[hostKey]; ok { - host.loadSystemUsers(u.User.ID) - return host.HasUniqueSu() - } - return "", false -} - -func (u *UserSftp) validatePermission(su *model.SystemUser, action string) bool { - for _, pemAction := range su.Actions { - if pemAction == action || pemAction == model.AllAction { - return true + u.Dirs[folderName] = &nodeDir + case "asset": + asset, err := model.ConvertMetaToAsset(body) + if err != nil { + logger.Errorf("convert to asset err: %s", err) + continue + } + if !asset.IsSupportProtocol("ssh") { + continue + } + assetDir := NewAssetDir(u.User, asset, u.Addr, u.logChan) + folderName := assetDir.folderName + for { + _, ok := u.Dirs[folderName] + if !ok { + break + } + folderName = fmt.Sprintf("%s_", folderName) + } + if folderName != assetDir.folderName { + assetDir.folderName = folderName + } + u.Dirs[folderName] = &assetDir } } - return false -} -func (u *UserSftp) CreateFTPLog(asset *model.Asset, su *model.SystemUser, operate, filename string, isSuccess bool) { - data := model.FTPLog{ - User: fmt.Sprintf("%s (%s)", u.User.Name, u.User.Username), - Hostname: asset.Hostname, - OrgID: asset.OrgID, - SystemUser: su.Name, - RemoteAddr: u.Addr, - Operate: operate, - Path: filename, - DataStart: common.CurrentUTCTime(), - IsSuccess: isSuccess, - } - u.LogChan <- &data } -func (u *UserSftp) LoopPushFTPLog() { +func (u *UserSftpConn) loopPushFTPLog() { ftpLogList := make([]*model.FTPLog, 0, 1024) - dataChan := make(chan *model.FTPLog) - go u.SendFTPLog(dataChan) - defer close(dataChan) - - tick := time.NewTicker(time.Second * 10) + maxRetry := 0 + var err error + tick := time.NewTicker(time.Second * 20) defer tick.Stop() for { select { + case <-u.closed: + if len(ftpLogList) == 0 { + return + } case <-tick.C: - case logData, ok := <-u.LogChan: + if len(ftpLogList) == 0 { + continue + } + case logData, ok := <-u.logChan: if !ok { return } ftpLogList = append(ftpLogList, logData) } - if len(ftpLogList) > 0 { - select { - case dataChan <- ftpLogList[len(ftpLogList)-1]: - ftpLogList = ftpLogList[:len(ftpLogList)-1] - default: - } - } - } -} - -func (u *UserSftp) SendFTPLog(dataChan <-chan *model.FTPLog) { - for data := range dataChan { - for i := 0; i < 4; i++ { - err := service.PushFTPLog(data) - if err == nil { - break - } - logger.Errorf("Create FTP log err: %s", err.Error()) - } - } -} - -func (u *UserSftp) GetSftpClient(asset *model.Asset, sysUser *model.SystemUser) (conn *SftpConn, err error) { - var ( - sshClient *SSHClient - ok bool - ) - - if u.ReuseConnection { - key := MakeReuseSSHClientKey(u.User, asset, sysUser) - switch sysUser.Username { - case "": - sshClient, ok = searchSSHClientFromCache(key) - if ok { - sysUser.Username = sshClient.username - } - default: - sshClient, ok = GetClientFromCache(key) - } - - if !ok { - sshClient, err = NewClient(u.User, asset, sysUser, u.Overtime, u.ReuseConnection) - if err != nil { - logger.Errorf("Get new SSH client err: %s", err) - return - } + data := ftpLogList[len(ftpLogList)-1] + err = service.PushFTPLog(data) + if err == nil { + ftpLogList = ftpLogList[:len(ftpLogList)-1] + maxRetry = 0 + continue } else { - logger.Infof("Reuse connection for SFTP: %s->%s@%s. SSH client %p current ref: %d", - u.User.Username, sshClient.username, asset.IP, sshClient, sshClient.RefCount()) + logger.Errorf("Create FTP log err: %s", err.Error()) } - } else { - sshClient, err = NewClient(u.User, asset, sysUser, u.Overtime, u.ReuseConnection) - if err != nil { - logger.Errorf("Get new SSH client err: %s", err) - return + if maxRetry > 5 { + ftpLogList = ftpLogList[1:] } + maxRetry++ } +} - sftpClient, err := sftp.NewClient(sshClient.client) - if err != nil { - logger.Errorf("SSH client %p start sftp client session err %s", sshClient, err) - RecycleClient(sshClient) - return nil, err +func (u *UserSftpConn) Search(key string) (res []os.FileInfo, err error) { + if u.searchDir == nil{ + logger.Errorf("not found search folder") + return nil, fmt.Errorf("not found") } - - HomeDirPath, err := sftpClient.Getwd() + assetsTree, err := service.SearchPermAsset(u.User.ID, key) if err != nil { - logger.Errorf("SSH client %p get home dir err %s", sshClient, err) - _ = sftpClient.Close() - RecycleClient(sshClient) + logger.Errorf("search asset err: %s", err) return nil, err } - logger.Infof("SSH client %p start sftp client session success", sshClient) - conn = &SftpConn{client: sftpClient, conn: sshClient, HomeDirPath: HomeDirPath} - return conn, err -} - -func (u *UserSftp) Close() { - for _, client := range u.sftpClients { - if client == nil { + subDirs := map[string]os.FileInfo{} + for _, item := range assetsTree { + typeName, ok := item.Meta["type"].(string) + if !ok { continue } - client.Close() - } - close(u.LogChan) -} - -type requestMessage struct { - host string - su string - dpath string - suUnique bool -} - -func NewHostnameDir(asset *model.Asset) *HostnameDir { - h := HostnameDir{asset: asset} - return &h -} - -type HostnameDir struct { - asset *model.Asset - suMaps map[string]*model.SystemUser -} - -func (h *HostnameDir) loadSystemUsers(userID string) { - if h.suMaps == nil { - sus := make(map[string]*model.SystemUser) - SystemUsers := service.GetUserAssetSystemUsers(userID, h.asset.ID) - for i := 0; i < len(SystemUsers); i++ { - if SystemUsers[i].Protocol == "ssh" { - sus[SystemUsers[i].Name] = &SystemUsers[i] + body, err := json.Marshal(item.Meta[typeName]) + if err != nil { + logger.Errorf("Search Json Marshal err: %s", err) + continue + } + switch typeName { + case "asset": + asset, err := model.ConvertMetaToAsset(body) + if err != nil { + logger.Errorf("convert to asset err: %s", err) + continue } + if !asset.IsSupportProtocol("ssh") { + continue + } + assetDir := NewAssetDir(u.User, asset, u.Addr, u.logChan) + folderName := assetDir.folderName + for { + _, ok := subDirs[folderName] + if !ok { + break + } + folderName = fmt.Sprintf("%s_", folderName) + } + if folderName != assetDir.folderName { + assetDir.folderName = folderName + } + subDirs[assetDir.folderName] = &assetDir } - h.suMaps = sus } + u.searchDir.SetSubDirs(subDirs) + return u.searchDir.List() } -func (h *HostnameDir) HasUniqueSu() (string, bool) { - sus := h.GetSystemUsers() - if len(sus) == 1 { - return sus[0].Name, true +func NewUserSftpConn(user *model.User, addr string) *UserSftpConn { + u := UserSftpConn{ + User: user, + Addr: addr, + Dirs: map[string]os.FileInfo{}, + modeTime: time.Now().UTC(), + logChan: make(chan *model.FTPLog, 1024), + closed: make(chan struct{}), } - return "", false -} - -func (h *HostnameDir) GetSystemUsers() (sus []model.SystemUser) { - sus = make([] model.SystemUser, 0, len(h.suMaps)) - for _, item := range h.suMaps { - sus = append(sus, *item) - } - model.SortSystemUserByPriority(sus) - return sus -} - -type SftpConn struct { - HomeDirPath string - client *sftp.Client - conn *SSHClient -} - -func (s *SftpConn) Close() { - if s.client == nil { - return - } - _ = s.client.Close() - RecycleClient(s.conn) -} - -func NewFakeFile(name string, isDir bool) *FakeFileInfo { - return &FakeFileInfo{ - name: name, - modtime: time.Now().UTC(), - isDir: isDir, - size: int64(0), - } -} - -func NewFakeSymFile(name string) *FakeFileInfo { - return &FakeFileInfo{ - name: name, - modtime: time.Now().UTC(), - size: int64(0), - symlink: name, - } -} - -type FakeFileInfo struct { - name string - isDir bool - size int64 - modtime time.Time - symlink string + u.initial() + go u.loopPushFTPLog() + return &u } -func (f *FakeFileInfo) Name() string { return f.name } -func (f *FakeFileInfo) Size() int64 { return f.size } -func (f *FakeFileInfo) Mode() os.FileMode { - ret := os.FileMode(0644) - if f.isDir { - ret = os.FileMode(0755) | os.ModeDir - } - if f.symlink != "" { - ret = os.FileMode(0777) | os.ModeSymlink +func NewUserSftpConnWithAssets(user *model.User, addr string, assets ...model.Asset) *UserSftpConn { + u := UserSftpConn{ + User: user, + Addr: addr, + Dirs: map[string]os.FileInfo{}, + modeTime: time.Now().UTC(), + logChan: make(chan *model.FTPLog, 1024), + closed: make(chan struct{}), + } + for _, asset := range assets { + if asset.IsSupportProtocol("ssh") { + assetDir := NewAssetDir(u.User, asset, u.Addr, u.logChan) + folderName := assetDir.folderName + for { + _, ok := u.Dirs[folderName] + if !ok { + break + } + folderName = fmt.Sprintf("%s_", folderName) + } + if folderName != assetDir.folderName { + assetDir.folderName = folderName + } + u.Dirs[assetDir.folderName] = &assetDir + } } - return ret -} -func (f *FakeFileInfo) ModTime() time.Time { return f.modtime } -func (f *FakeFileInfo) IsDir() bool { return f.isDir } -func (f *FakeFileInfo) Sys() interface{} { - return &syscall.Stat_t{Uid: 0, Gid: 0} -} - -type FileInfoList []os.FileInfo - -func (fl FileInfoList) Len() int { - return len(fl) + go u.loopPushFTPLog() + return &u } -func (fl FileInfoList) Swap(i, j int) { fl[i], fl[j] = fl[j], fl[i] } -func (fl FileInfoList) Less(i, j int) bool { return fl[i].Name() < fl[j].Name() } diff --git a/jumpserver/koko/pkg/srvconn/sftpfile.go b/jumpserver/koko/pkg/srvconn/sftpfile.go new file mode 100644 index 0000000000000000000000000000000000000000..964a6000abf705197a0ded2d905609081db3d372 --- /dev/null +++ b/jumpserver/koko/pkg/srvconn/sftpfile.go @@ -0,0 +1,821 @@ +package srvconn + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "sync" + "syscall" + "time" + + "github.com/pkg/sftp" + + "github.com/jumpserver/koko/pkg/common" + "github.com/jumpserver/koko/pkg/config" + "github.com/jumpserver/koko/pkg/logger" + "github.com/jumpserver/koko/pkg/model" + "github.com/jumpserver/koko/pkg/service" +) + +const ( + SearchFolderName = "_Search" +) + +type SearchResultDir struct { + subDirs map[string]os.FileInfo + folderName string + modeTime time.Time +} + +func (sd *SearchResultDir) Name() string { + return sd.folderName +} + +func (sd *SearchResultDir) Size() int64 { return 0 } + +func (sd *SearchResultDir) Mode() os.FileMode { + return os.ModePerm | os.ModeDir +} + +func (sd *SearchResultDir) ModTime() time.Time { return sd.modeTime } + +func (sd *SearchResultDir) IsDir() bool { return true } + +func (sd *SearchResultDir) Sys() interface{} { + return &syscall.Stat_t{Uid: 0, Gid: 0} +} + +func (sd *SearchResultDir) List() (res []os.FileInfo, err error) { + for _, item := range sd.subDirs { + res = append(res, item) + } + return +} + +func (sd *SearchResultDir) SetSubDirs(subDirs map[string]os.FileInfo) { + if sd.subDirs != nil { + for _, dir := range sd.subDirs { + if assetDir, ok := dir.(*AssetDir); ok { + assetDir.close() + } + } + } + sd.subDirs = subDirs +} + +func (sd *SearchResultDir) close() { + for _, dir := range sd.subDirs { + if assetDir, ok := dir.(*AssetDir); ok { + assetDir.close() + } + } +} + +type NodeDir struct { + node *model.Node + subDirs map[string]os.FileInfo + folderName string + modeTime time.Time + + once *sync.Once +} + +func (nd *NodeDir) Name() string { + return nd.folderName +} + +func (nd *NodeDir) Size() int64 { return 0 } + +func (nd *NodeDir) Mode() os.FileMode { + return os.ModePerm | os.ModeDir +} +func (nd *NodeDir) ModTime() time.Time { return nd.modeTime } + +func (nd *NodeDir) IsDir() bool { return true } + +func (nd *NodeDir) Sys() interface{} { + return &syscall.Stat_t{Uid: 0, Gid: 0} +} + +func (nd *NodeDir) List() (res []os.FileInfo, err error) { + for _, item := range nd.subDirs { + res = append(res, item) + } + return +} + +func (nd *NodeDir) loadNodeAsset(uSftp *UserSftpConn) { + nd.once.Do(func() { + nodeTrees := service.GetUserNodeTreeWithAsset(uSftp.User.ID, nd.node.ID, "1") + dirs := map[string]os.FileInfo{} + for _, item := range nodeTrees { + typeName, ok := item.Meta["type"].(string) + if !ok { + continue + } + body, err := json.Marshal(item.Meta[typeName]) + if err != nil { + continue + } + switch typeName { + case "node": + node, err := model.ConvertMetaToNode(body) + if err != nil { + logger.Errorf("convert node err: %s", err) + continue + } + nodeDir := NewNodeDir(node) + folderName := nodeDir.folderName + for { + _, ok := dirs[folderName] + if !ok { + break + } + folderName = fmt.Sprintf("%s_", folderName) + } + if folderName != nodeDir.folderName { + nodeDir.folderName = folderName + } + + dirs[folderName] = &nodeDir + case "asset": + asset, err := model.ConvertMetaToAsset(body) + if err != nil { + logger.Errorf("convert asset err: %s", err) + continue + } + if !asset.IsSupportProtocol("ssh") { + continue + } + assetDir := NewAssetDir(uSftp.User, asset, uSftp.Addr, uSftp.logChan) + folderName := assetDir.folderName + for { + _, ok := dirs[folderName] + if !ok { + break + } + folderName = fmt.Sprintf("%s_", folderName) + } + if folderName != assetDir.folderName { + assetDir.folderName = folderName + } + dirs[folderName] = &assetDir + } + } + nd.subDirs = dirs + }) +} + +func (nd *NodeDir) close() { + for _, dir := range nd.subDirs { + if nodeDir, ok := dir.(*NodeDir); ok { + nodeDir.close() + continue + } + if assetDir, ok := dir.(*AssetDir); ok { + assetDir.close() + } + + } +} + +func NewNodeDir(node model.Node) NodeDir { + folderName := node.Value + if strings.Contains(node.Value, "/") { + folderName = strings.ReplaceAll(node.Value, "/", "_") + } + return NodeDir{ + node: &node, + folderName: folderName, + subDirs: map[string]os.FileInfo{}, + modeTime: time.Now().UTC(), + once: new(sync.Once), + } +} + +func NewAssetDir(user *model.User, asset model.Asset, addr string, logChan chan<- *model.FTPLog) AssetDir { + folderName := asset.Hostname + if strings.Contains(folderName, "/") { + folderName = strings.ReplaceAll(folderName, "/", "_") + } + conf := config.GetConf() + return AssetDir{ + user: user, + asset: &asset, + folderName: folderName, + modeTime: time.Now().UTC(), + addr: addr, + suMaps: nil, + logChan: logChan, + Overtime: conf.SSHTimeout * time.Second, + RootPath: conf.SftpRoot, + ShowHidden: conf.ShowHiddenFile, + reuse: conf.ReuseConnection, + sftpClients: map[string]*SftpConn{}, + } +} + +type AssetDir struct { + user *model.User + asset *model.Asset + folderName string + modeTime time.Time + addr string + + suMaps map[string]*model.SystemUser + + logChan chan<- *model.FTPLog + + sftpClients map[string]*SftpConn // systemUser_id + + once sync.Once + + RootPath string + reuse bool + ShowHidden bool + Overtime time.Duration +} + +func (ad *AssetDir) Name() string { + return ad.folderName +} + +func (ad *AssetDir) Size() int64 { return 0 } + +func (ad *AssetDir) Mode() os.FileMode { + return os.ModePerm | os.ModeDir +} + +func (ad *AssetDir) ModTime() time.Time { return ad.modeTime } + +func (ad *AssetDir) IsDir() bool { return true } + +func (ad *AssetDir) Sys() interface{} { + return &syscall.Stat_t{Uid: 0, Gid: 0} +} + +func (ad *AssetDir) loadSystemUsers() { + ad.once.Do(func() { + sus := make(map[string]*model.SystemUser) + SystemUsers := service.GetUserAssetSystemUsers(ad.user.ID, ad.asset.ID) + for i := 0; i < len(SystemUsers); i++ { + if SystemUsers[i].Protocol == "ssh" { + ok := true + folderName := SystemUsers[i].Name + for ok { + if _, ok = sus[folderName]; ok { + folderName = fmt.Sprintf("%s_", folderName) + } + } + sus[folderName] = &SystemUsers[i] + } + } + ad.suMaps = sus + }) +} + +func (ad *AssetDir) Create(path string) (*sftp.File, error) { + pathData := ad.parsePath(path) + folderName, ok := ad.IsUniqueSu() + if !ok { + if len(pathData) == 1 && pathData[0] == "" { + return nil, sftp.ErrSshFxPermissionDenied + } + folderName = pathData[0] + pathData = pathData[1:] + } + su, ok := ad.suMaps[folderName] + if !ok { + return nil, sftp.ErrSshFxNoSuchFile + } + if !ad.validatePermission(su, model.UploadAction) { + return nil, sftp.ErrSshFxPermissionDenied + } + + con, realPath := ad.GetSFTPAndRealPath(su, strings.Join(pathData, "/")) + if con == nil { + return nil, sftp.ErrSshFxConnectionLost + } + sf, err := con.client.Create(realPath) + filename := realPath + isSuccess := false + operate := model.OperateUpload + if err == nil { + isSuccess = true + } + ad.CreateFTPLog(su, operate, filename, isSuccess) + return sf, err +} + +func (ad *AssetDir) MkdirAll(path string) (err error) { + pathData := ad.parsePath(path) + folderName, ok := ad.IsUniqueSu() + if !ok { + if len(pathData) == 1 && pathData[0] == "" { + return sftp.ErrSshFxPermissionDenied + } + folderName = pathData[0] + pathData = pathData[1:] + } + su, ok := ad.suMaps[folderName] + if !ok { + return sftp.ErrSshFxNoSuchFile + } + if !ad.validatePermission(su, model.UploadAction) { + return sftp.ErrSshFxPermissionDenied + } + + con, realPath := ad.GetSFTPAndRealPath(su, strings.Join(pathData, "/")) + if con == nil { + return sftp.ErrSshFxConnectionLost + } + err = con.client.MkdirAll(realPath) + filename := realPath + isSuccess := false + operate := model.OperateMkdir + if err == nil { + isSuccess = true + } + ad.CreateFTPLog(su, operate, filename, isSuccess) + return +} + +func (ad *AssetDir) Open(path string) (*sftp.File, error) { + pathData := ad.parsePath(path) + folderName, ok := ad.IsUniqueSu() + if !ok { + if len(pathData) == 1 && pathData[0] == "" { + return nil, sftp.ErrSshFxPermissionDenied + } + folderName = pathData[0] + pathData = pathData[1:] + } + su, ok := ad.suMaps[folderName] + if !ok { + return nil, sftp.ErrSshFxNoSuchFile + } + if !ad.validatePermission(su, model.DownloadAction) { + return nil, sftp.ErrSshFxPermissionDenied + } + con, realPath := ad.GetSFTPAndRealPath(su, strings.Join(pathData, "/")) + if con == nil { + return nil, sftp.ErrSshFxConnectionLost + } + sf, err := con.client.Open(realPath) + filename := realPath + isSuccess := false + operate := model.OperateDownaload + if err == nil { + isSuccess = true + } + ad.CreateFTPLog(su, operate, filename, isSuccess) + return sf, err +} + +func (ad *AssetDir) ReadDir(path string) (res []os.FileInfo, err error) { + pathData := ad.parsePath(path) + folderName, ok := ad.IsUniqueSu() + if !ok { + if len(pathData) == 1 && pathData[0] == "" { + for folderName := range ad.suMaps { + res = append(res, NewFakeFile(folderName, true)) + } + return + } + folderName = pathData[0] + pathData = pathData[1:] + } + su, ok := ad.suMaps[folderName] + if !ok { + return nil, sftp.ErrSshFxNoSuchFile + } + if !ad.validatePermission(su, model.ConnectAction) { + return res, sftp.ErrSshFxPermissionDenied + } + + con, realPath := ad.GetSFTPAndRealPath(su, strings.Join(pathData, "/")) + if con == nil { + return nil, sftp.ErrSshFxConnectionLost + } + res, err = con.client.ReadDir(realPath) + if !ad.ShowHidden { + noHiddenFiles := make([]os.FileInfo, 0, len(res)) + for i := 0; i < len(res); i++ { + if !strings.HasPrefix(res[i].Name(), ".") { + noHiddenFiles = append(noHiddenFiles, res[i]) + } + } + return noHiddenFiles, err + } + return +} + +func (ad *AssetDir) ReadLink(path string) (res string, err error) { + pathData := ad.parsePath(path) + if len(pathData) == 1 && pathData[0] == "" { + return "", sftp.ErrSshFxOpUnsupported + } + folderName, ok := ad.IsUniqueSu() + if !ok { + folderName = pathData[0] + pathData = pathData[1:] + } + su, ok := ad.suMaps[folderName] + if !ok { + return "", sftp.ErrSshFxNoSuchFile + } + if !ad.validatePermission(su, model.ConnectAction) { + return res, sftp.ErrSshFxPermissionDenied + } + + con, realPath := ad.GetSFTPAndRealPath(su, strings.Join(pathData, "/")) + if con == nil { + return "", sftp.ErrSshFxConnectionLost + } + res, err = con.client.ReadLink(realPath) + return +} + +func (ad *AssetDir) RemoveDirectory(path string) (err error) { + pathData := ad.parsePath(path) + folderName, ok := ad.IsUniqueSu() + if !ok { + if len(pathData) == 1 && pathData[0] == "" { + return sftp.ErrSshFxPermissionDenied + } + folderName = pathData[0] + pathData = pathData[1:] + } + su, ok := ad.suMaps[folderName] + if !ok { + return sftp.ErrSshFxNoSuchFile + } + if !ad.validatePermission(su, model.UploadAction) { + return sftp.ErrSshFxPermissionDenied + } + con, realPath := ad.GetSFTPAndRealPath(su, strings.Join(pathData, "/")) + if con == nil { + return sftp.ErrSshFxConnectionLost + } + err = ad.removeDirectoryAll(con.client, realPath) + filename := realPath + isSuccess := false + operate := model.OperateRemoveDir + if err == nil { + isSuccess = true + } + ad.CreateFTPLog(su, operate, filename, isSuccess) + return +} + +func (ad *AssetDir) Rename(oldNamePath, newNamePath string) (err error) { + oldPathData := ad.parsePath(oldNamePath) + newPathData := ad.parsePath(newNamePath) + + folderName, ok := ad.IsUniqueSu() + if !ok { + if oldPathData[0] != newPathData[0] { + return sftp.ErrSshFxNoSuchFile + } + folderName = oldPathData[0] + oldPathData = oldPathData[1:] + newPathData = newPathData[1:] + } + su, ok := ad.suMaps[folderName] + if !ok { + return sftp.ErrSshFxNoSuchFile + } + conn1, oldRealPath := ad.GetSFTPAndRealPath(su, strings.Join(oldPathData, "/")) + conn2, newRealPath := ad.GetSFTPAndRealPath(su, strings.Join(newPathData, "/")) + if conn1 != conn2 { + return sftp.ErrSshFxOpUnsupported + } + + err = conn1.client.Rename(oldRealPath, newRealPath) + + filename := fmt.Sprintf("%s=>%s", oldRealPath, newRealPath) + isSuccess := false + operate := model.OperateRename + if err == nil { + isSuccess = true + } + ad.CreateFTPLog(su, operate, filename, isSuccess) + return +} + +func (ad *AssetDir) Remove(path string) (err error) { + pathData := ad.parsePath(path) + folderName, ok := ad.IsUniqueSu() + if !ok { + if len(pathData) == 1 && pathData[0] == "" { + return sftp.ErrSshFxPermissionDenied + } + folderName = pathData[0] + pathData = pathData[1:] + } + su, ok := ad.suMaps[folderName] + if !ok { + return sftp.ErrSshFxNoSuchFile + } + if !ad.validatePermission(su, model.UploadAction) { + return sftp.ErrSshFxPermissionDenied + } + con, realPath := ad.GetSFTPAndRealPath(su, strings.Join(pathData, "/")) + if con == nil { + return sftp.ErrSshFxConnectionLost + } + err = con.client.Remove(realPath) + + filename := realPath + isSuccess := false + operate := model.OperateDelete + if err == nil { + isSuccess = true + } + ad.CreateFTPLog(su, operate, filename, isSuccess) + return +} + +func (ad *AssetDir) Stat(path string) (res os.FileInfo, err error) { + pathData := ad.parsePath(path) + if len(pathData) == 1 && pathData[0] == "" { + return ad, nil + } + folderName, ok := ad.IsUniqueSu() + if !ok { + folderName = pathData[0] + pathData = pathData[1:] + } + su, ok := ad.suMaps[folderName] + if !ok { + return nil, sftp.ErrSshFxNoSuchFile + } + if !ad.validatePermission(su, model.ConnectAction) { + return res, sftp.ErrSshFxPermissionDenied + } + con, realPath := ad.GetSFTPAndRealPath(su, strings.Join(pathData, "/")) + if con == nil { + return nil, sftp.ErrSshFxConnectionLost + } + res, err = con.client.Stat(realPath) + return +} + +func (ad *AssetDir) Symlink(oldNamePath, newNamePath string) (err error) { + oldPathData := ad.parsePath(oldNamePath) + newPathData := ad.parsePath(newNamePath) + + folderName, ok := ad.IsUniqueSu() + if !ok { + if oldPathData[0] != newPathData[0] { + return sftp.ErrSshFxNoSuchFile + } + folderName = oldPathData[0] + oldPathData = oldPathData[1:] + newPathData = newPathData[1:] + } + su, ok := ad.suMaps[folderName] + if !ok { + return sftp.ErrSshFxNoSuchFile + } + if !ad.validatePermission(su, model.UploadAction) { + return sftp.ErrSshFxPermissionDenied + } + conn1, oldRealPath := ad.GetSFTPAndRealPath(su, strings.Join(oldPathData, "/")) + conn2, newRealPath := ad.GetSFTPAndRealPath(su, strings.Join(newPathData, "/")) + if conn1 != conn2 { + return sftp.ErrSshFxOpUnsupported + } + err = conn1.client.Symlink(oldRealPath, newRealPath) + filename := fmt.Sprintf("%s=>%s", oldRealPath, newRealPath) + isSuccess := false + operate := model.OperateSymlink + if err == nil { + isSuccess = true + } + ad.CreateFTPLog(su, operate, filename, isSuccess) + return +} + +func (ad *AssetDir) removeDirectoryAll(conn *sftp.Client, path string) error { + var err error + var files []os.FileInfo + files, err = conn.ReadDir(path) + if err != nil { + return err + } + for _, item := range files { + realPath := filepath.Join(path, item.Name()) + + if item.IsDir() { + err = ad.removeDirectoryAll(conn, realPath) + if err != nil { + return err + } + continue + } + err = conn.Remove(realPath) + if err != nil { + return err + } + } + return conn.RemoveDirectory(path) +} + +func (ad *AssetDir) GetSFTPAndRealPath(su *model.SystemUser, path string) (conn *SftpConn, realPath string) { + var ok bool + conn, ok = ad.sftpClients[su.ID] + if !ok { + var err error + conn, err = ad.GetSftpClient(su) + if err != nil { + logger.Errorf("Get Sftp Client err: %s", err.Error()) + return nil, "" + } + ad.sftpClients[su.ID] = conn + } + + switch strings.ToLower(ad.RootPath) { + case "home", "~", "": + realPath = filepath.Join(conn.HomeDirPath, strings.TrimPrefix(path, "/")) + default: + realPath = filepath.Join(ad.RootPath, strings.TrimPrefix(path, "/")) + } + return +} + +func (ad *AssetDir) IsUniqueSu() (folderName string, ok bool) { + sus := ad.getSubFolderNames() + if len(sus) == 1 { + return sus[0], true + } + return +} + +func (ad *AssetDir) getSubFolderNames() []string { + sus := make([]string, 0, len(ad.suMaps)) + for folderName := range ad.suMaps { + sus = append(sus, folderName) + } + return sus +} + +func (ad *AssetDir) validatePermission(su *model.SystemUser, action string) bool { + for _, pemAction := range su.Actions { + if pemAction == action || pemAction == model.AllAction { + return true + } + } + return false +} + +func (ad *AssetDir) GetSftpClient(su *model.SystemUser) (conn *SftpConn, err error) { + var ( + sshClient *SSHClient + ok bool + ) + if ad.reuse { + key := MakeReuseSSHClientKey(ad.user, ad.asset, su) + switch su.Username { + case "": + sshClient, ok = searchSSHClientFromCache(key) + if ok { + su.Username = sshClient.username + } + default: + sshClient, ok = GetClientFromCache(key) + } + + if !ok { + sshClient, err = NewClient(ad.user, ad.asset, su, ad.Overtime, ad.reuse) + if err != nil { + logger.Errorf("Get new SSH client err: %s", err) + return + } + } else { + logger.Infof("Reuse connection for SFTP: %s->%s@%s. SSH client %p current ref: %d", + ad.user.Username, sshClient.username, ad.asset.IP, sshClient, sshClient.RefCount()) + } + + } else { + sshClient, err = NewClient(ad.user, ad.asset, su, ad.Overtime, ad.reuse) + if err != nil { + logger.Errorf("Get new SSH client err: %s", err) + return + } + } + + sftpClient, err := sftp.NewClient(sshClient.client) + if err != nil { + logger.Errorf("SSH client %p start sftp client session err %s", sshClient, err) + RecycleClient(sshClient) + return nil, err + } + + HomeDirPath, err := sftpClient.Getwd() + if err != nil { + logger.Errorf("SSH client %p get home dir err %s", sshClient, err) + _ = sftpClient.Close() + RecycleClient(sshClient) + return nil, err + } + logger.Infof("SSH client %p start sftp client session success", sshClient) + conn = &SftpConn{client: sftpClient, conn: sshClient, HomeDirPath: HomeDirPath} + return conn, err +} + +func (ad *AssetDir) parsePath(path string) []string { + path = strings.TrimPrefix(path, "/") + return strings.Split(path, "/") +} + +func (ad *AssetDir) close() { + for _, conn := range ad.sftpClients { + if conn != nil { + conn.Close() + } + } +} + +func (ad *AssetDir) CreateFTPLog(su *model.SystemUser, operate, filename string, isSuccess bool) { + data := model.FTPLog{ + User: fmt.Sprintf("%s (%s)", ad.user.Name, ad.user.Username), + Hostname: ad.asset.Hostname, + OrgID: ad.asset.OrgID, + SystemUser: su.Name, + RemoteAddr: ad.addr, + Operate: operate, + Path: filename, + DataStart: common.CurrentUTCTime(), + IsSuccess: isSuccess, + } + ad.logChan <- &data +} + +type SftpConn struct { + HomeDirPath string + client *sftp.Client + conn *SSHClient +} + +func (s *SftpConn) Close() { + if s.client == nil { + return + } + _ = s.client.Close() + RecycleClient(s.conn) +} + +func NewFakeFile(name string, isDir bool) *FakeFileInfo { + return &FakeFileInfo{ + name: name, + modTime: time.Now().UTC(), + isDir: isDir, + size: int64(0), + } +} + +func NewFakeSymFile(name string) *FakeFileInfo { + return &FakeFileInfo{ + name: name, + modTime: time.Now().UTC(), + size: int64(0), + symlink: name, + } +} + +type FakeFileInfo struct { + name string + isDir bool + size int64 + modTime time.Time + symlink string +} + +func (f *FakeFileInfo) Name() string { return f.name } +func (f *FakeFileInfo) Size() int64 { return f.size } +func (f *FakeFileInfo) Mode() os.FileMode { + ret := os.FileMode(0644) + if f.isDir { + ret = os.FileMode(0755) | os.ModeDir + } + if f.symlink != "" { + ret = os.FileMode(0777) | os.ModeSymlink + } + return ret +} +func (f *FakeFileInfo) ModTime() time.Time { return f.modTime } +func (f *FakeFileInfo) IsDir() bool { return f.isDir } +func (f *FakeFileInfo) Sys() interface{} { + return &syscall.Stat_t{Uid: 0, Gid: 0} +} + +type FileInfoList []os.FileInfo + +func (fl FileInfoList) Len() int { + return len(fl) +} +func (fl FileInfoList) Swap(i, j int) { fl[i], fl[j] = fl[j], fl[i] } +func (fl FileInfoList) Less(i, j int) bool { return fl[i].Name() < fl[j].Name() } diff --git a/jumpserver/koko/pkg/srvconn/telnetconn.go b/jumpserver/koko/pkg/srvconn/telnetconn.go index 6d18420694f83be4de741a5487e5bf69daa74adb..a6ef8d0a95243b11686ee08ec2ce910620f61c9b 100644 --- a/jumpserver/koko/pkg/srvconn/telnetconn.go +++ b/jumpserver/koko/pkg/srvconn/telnetconn.go @@ -132,7 +132,7 @@ func (tc *ServerTelnetConnection) login(data []byte) AuthStatus { return AuthSuccess } if tc.CustomString != "" { - if tc.CustomSuccessPattern.Match(data) { + if tc.CustomSuccessPattern != nil && tc.CustomSuccessPattern.Match(data) { return AuthSuccess } } diff --git a/jumpserver/koko/pkg/utils/database.go b/jumpserver/koko/pkg/utils/database.go new file mode 100644 index 0000000000000000000000000000000000000000..d4b82bf563fd99796c82b7151a0c07f0233cca11 --- /dev/null +++ b/jumpserver/koko/pkg/utils/database.go @@ -0,0 +1,24 @@ +package utils + +import ( + "os/exec" + "os/user" +) + +func IsInstalledMysqlClient() bool { + if mysqlPath, err := exec.LookPath("mysql"); err == nil { + cmd := exec.Command(mysqlPath, "-V") + if err = cmd.Start(); err == nil { + _ = cmd.Process.Kill() + return true + } + } + return false +} + +func IsUserExist(username string) bool { + if _, err := user.Lookup(username); err == nil { + return true + } + return false +} diff --git a/jumpserver/koko/pkg/utils/parser.go b/jumpserver/koko/pkg/utils/parser.go index 6b7f53f89ef347241621d118204c9e59f987e2c6..cfd47e92dfca7f29674d98f75f784d56370a849e 100644 --- a/jumpserver/koko/pkg/utils/parser.go +++ b/jumpserver/koko/pkg/utils/parser.go @@ -1,194 +1,306 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + package utils import ( "bytes" - "fmt" "unicode/utf8" ) -func ParseTerminalData(p []byte) (lines []string) { - c := bytes.NewReader(p) - pasteActive := false - var line []rune - var pos int - var remainder []byte - var inBuf [256]byte - for { - rest := remainder - lineOk := false - for !lineOk { - var key rune - key, rest = bytesToKey(rest, pasteActive) - if key == utf8.RuneError { - break - } - if !pasteActive { - if key == keyPasteStart { - pasteActive = true - if len(line) == 0 { - } - continue - } - } else if key == keyPasteEnd { - pasteActive = false - continue - } +type terminalParser struct { - switch key { - case keyBackspace: - if pos == 0 { - continue - } - line, pos = EraseNPreviousChars(1, pos, line) - case keyAltLeft: - // move left by a word. - pos -= CountToLeftWord(pos, line) - case keyAltRight: - // move right by a word. - pos += CountToRightWord(pos, line) - case keyLeft: - if pos == 0 { - continue - } - pos-- - case keyRight: - if pos == len(line) { - continue - } - pos++ - case keyHome: - if pos == 0 { - continue - } - pos = 0 - case keyEnd: - if pos == len(line) { - continue - } - pos = len(line) - case keyUp: - line = []rune{} - pos = 0 - case keyDown: - line = []rune{} - pos = 0 - case keyEnter: - lines = append(lines, string(line)) - line = line[:0] - pos = 0 - lineOk = true - case keyDeleteWord: - // Delete zero or more spaces and then one or more characters. - line, pos = EraseNPreviousChars(CountToLeftWord(pos, line), pos, line) - case keyDeleteLine: - line = line[:pos] - case keyCtrlD: - // Erase the character under the current position. - // The EOF case when the line is empty is handled in - // readLine(). - if pos < len(line) { - pos++ - line, pos = EraseNPreviousChars(1, pos, line) - } - case keyCtrlU: - line = line[:0] - case keyClearScreen: - default: - if !isPrintable(key) { - fmt.Println("could not printable: ", []byte(string(key)), " ", key) - continue - } - line, pos = AddKeyToLine(key, pos, line) - } + // line is the current line being entered. + line []rune + // pos is the logical position of the cursor in line + pos int + // pasteActive is true iff there is a bracketed paste operation in + // progress. + pasteActive bool - } - if len(rest) > 0 { - n := copy(inBuf[:], rest) - remainder = inBuf[:n] - } else { - remainder = nil - } + // maxLine is the greatest value of cursorY so far. + maxLine int - // remainder is a slice at the beginning of t.inBuf - // containing a partial key sequence - readBuf := inBuf[len(remainder):] + // remainder contains the remainder of any partial key sequences after + // a read. It aliases into inBuf. + remainder []byte + inBuf [256]byte - var n int - n, err := c.Read(readBuf) - if err != nil { - if len(line) > 0 { - lines = append(lines, string(line)) - } else if len(rest) > 0 { - lines = append(lines, string(rest)) - } + // history contains previously entered commands so that they can be + // accessed with the up and down keys. + history stRingBuffer + // historyIndex stores the currently accessed history entry, where zero + // means the immediately previous entry. + historyIndex int + // When navigating up and down the history it's possible to return to + // the incomplete, initial line. That value is stored in + // historyPending. + historyPending string +} - return - } - remainder = inBuf[:n+len(remainder)] - } +func (t *terminalParser) setLine(newLine []rune, newPos int) { + t.line = newLine + t.pos = newPos } -func EraseNPreviousChars(n, cPos int, line []rune) ([]rune, int) { +func (t *terminalParser) eraseNPreviousChars(n int) { if n == 0 { - return line, cPos + return } - if cPos < n { - n = cPos + + if t.pos < n { + n = t.pos } - cPos -= n - copy(line[cPos:], line[n+cPos:]) - return line[:len(line)-n], cPos + t.pos -= n + + copy(t.line[t.pos:], t.line[n+t.pos:]) + t.line = t.line[:len(t.line)-n] } -func CountToLeftWord(currentPos int, line []rune) int { - if currentPos == 0 { +// countToLeftWord returns then number of characters from the cursor to the +// start of the previous word. +func (t *terminalParser) countToLeftWord() int { + if t.pos == 0 { return 0 } - pos := currentPos - 1 + pos := t.pos - 1 for pos > 0 { - if line[pos] != ' ' { + if t.line[pos] != ' ' { break } pos-- } for pos > 0 { - if line[pos] == ' ' { + if t.line[pos] == ' ' { pos++ break } pos-- } - return currentPos - pos + return t.pos - pos } -func CountToRightWord(currentPos int, line []rune) int { - pos := currentPos - for pos < len(line) { - if line[pos] == ' ' { +// countToRightWord returns then number of characters from the cursor to the +// start of the next word. +func (t *terminalParser) countToRightWord() int { + pos := t.pos + for pos < len(t.line) { + if t.line[pos] == ' ' { break } pos++ } - for pos < len(line) { - if line[pos] != ' ' { + for pos < len(t.line) { + if t.line[pos] != ' ' { break } pos++ } - return pos - currentPos + return pos - t.pos +} + +// handleKey processes the given key and, optionally, returns a line of text +// that the user has entered. +func (t *terminalParser) handleKey(key rune) (line string, ok bool) { + if t.pasteActive && key != keyEnter { + t.addKeyToLine(key) + return + } + + switch key { + case keyBackspace: + if t.pos == 0 { + return + } + t.eraseNPreviousChars(1) + case keyAltLeft: + // move left by a word. + t.pos -= t.countToLeftWord() + case keyAltRight: + // move right by a word. + t.pos += t.countToRightWord() + case keyLeft: + if t.pos == 0 { + return + } + t.pos-- + case keyRight: + if t.pos == len(t.line) { + return + } + t.pos++ + case keyHome: + if t.pos == 0 { + return + } + t.pos = 0 + case keyEnd: + if t.pos == len(t.line) { + return + } + t.pos = len(t.line) + case keyUp: + entry, ok := t.history.NthPreviousEntry(t.historyIndex + 1) + if !ok { + return "", false + } + if t.historyIndex == -1 { + t.historyPending = string(t.line) + } + t.historyIndex++ + runes := []rune(entry) + t.setLine(runes, len(runes)) + case keyDown: + switch t.historyIndex { + case -1: + return + case 0: + runes := []rune(t.historyPending) + t.setLine(runes, len(runes)) + t.historyIndex-- + default: + entry, ok := t.history.NthPreviousEntry(t.historyIndex - 1) + if ok { + t.historyIndex-- + runes := []rune(entry) + t.setLine(runes, len(runes)) + } + } + case keyEnter: + line = string(t.line) + ok = true + t.line = t.line[:0] + t.pos = 0 + t.maxLine = 0 + case keyDeleteWord: + // Delete zero or more spaces and then one or more characters. + t.eraseNPreviousChars(t.countToLeftWord()) + case keyDeleteLine: + t.line = t.line[:t.pos] + case keyCtrlD: + // Erase the character under the current position. + // The EOF case when the line is empty is handled in + // readLine(). + if t.pos < len(t.line) { + t.pos++ + t.eraseNPreviousChars(1) + } + case keyCtrlU: + t.eraseNPreviousChars(t.pos) + case keyClearScreen: + // Erases the screen and moves the cursor to the home position. + t.setLine(t.line, t.pos) + default: + if !isPrintable(key) { + return + } + if len(t.line) == maxLineLength { + return + } + t.addKeyToLine(key) + } + return +} + +// addKeyToLine inserts the given key at the current position in the current +// line. +func (t *terminalParser) addKeyToLine(key rune) { + if len(t.line) == cap(t.line) { + newLine := make([]rune, len(t.line), 2*(1+len(t.line))) + copy(newLine, t.line) + t.line = newLine + } + t.line = t.line[:len(t.line)+1] + copy(t.line[t.pos+1:], t.line[t.pos:]) + t.line[t.pos] = key + t.pos++ +} + +func (t *terminalParser) parseLines(p []byte) (lines []string) { + var err error + + lines = make([]string, 0, 3) + lineIsPasted := t.pasteActive + reader := bytes.NewBuffer(p) + for { + rest := t.remainder + line := "" + lineOk := false + for !lineOk { + var key rune + key, rest = bytesToKey(rest, t.pasteActive) + if key == utf8.RuneError { + break + } + if !t.pasteActive { + if key == keyCtrlD { + if len(t.line) == 0 { + // as key has already handled, we need update remainder data, + t.remainder = rest + return lines + } + } + if key == keyPasteStart { + t.pasteActive = true + if len(t.line) == 0 { + lineIsPasted = true + } + continue + } + } else if key == keyPasteEnd { + t.pasteActive = false + continue + } + if !t.pasteActive { + lineIsPasted = false + } + line, lineOk = t.handleKey(key) + } + if len(rest) > 0 { + n := copy(t.inBuf[:], rest) + t.remainder = t.inBuf[:n] + } else { + t.remainder = nil + } + if lineOk { + if lineIsPasted { + err = ErrPasteIndicator + } + lines = append(lines, line) + } + + // t.remainder is a slice at the beginning of t.inBuf + // containing a partial key sequence + readBuf := t.inBuf[len(t.remainder):] + var n int + + n, err = reader.Read(readBuf) + if err != nil && n == 0 { + if len(t.line) > 0 && len(t.remainder) == 0 { + lines = append(lines, string(t.line)) + } + if len(t.remainder) > 0 { + t.remainder = t.remainder[1:] + continue + } + return + } else if err == nil && n == 0 { + if len(t.remainder) == len(t.inBuf) { + t.remainder = t.remainder[1:] + continue + } + } + + t.remainder = t.inBuf[:n+len(t.remainder)] + } } -func AddKeyToLine(key rune, pos int, line []rune) ([]rune, int) { - if len(line) == cap(line) { - newLine := make([]rune, len(line), 2*(1+len(line))) - copy(newLine, line) - line = newLine +func ParseTerminalData(p []byte) (lines []string) { + t := terminalParser{ + historyIndex: -1, } - line = line[:len(line)+1] - copy(line[pos+1:], line[pos:]) - line[pos] = key - pos++ - return line, pos -} \ No newline at end of file + return t.parseLines(p) +} diff --git a/jumpserver/luna/proxy.conf.json b/jumpserver/luna/proxy.conf.json index 5f8910f2751c047564401f17f3133f269f9b0567..8722291b7dc97aa1f2d387e27dbdbd01b649cadd 100644 --- a/jumpserver/luna/proxy.conf.json +++ b/jumpserver/luna/proxy.conf.json @@ -13,7 +13,7 @@ "secure": false }, "/guacamole/": { - "target": "http://127.0.0.1:8083", + "target": "http://127.0.0.1:8081", "secure": false, "ws": true, "pathRewrite": { diff --git a/jumpserver/luna/src/app/elements/asset-tree/asset-tree.component.html b/jumpserver/luna/src/app/elements/asset-tree/asset-tree.component.html index 91bffc2914b5fd05df6b2f9b83009c697391b539..511de8f65f2b2479f8dcb24e258b7c056b3fdd62 100644 --- a/jumpserver/luna/src/app/elements/asset-tree/asset-tree.component.html +++ b/jumpserver/luna/src/app/elements/asset-tree/asset-tree.component.html @@ -5,6 +5,9 @@
    + +
      +
    diff --git a/jumpserver/luna/src/app/elements/asset-tree/asset-tree.component.ts b/jumpserver/luna/src/app/elements/asset-tree/asset-tree.component.ts index 24cd4fe56d3de05b5079d1f0975a3cf14898ce49..9ed7ba78b9945ed85b78312b112872e4085da990 100644 --- a/jumpserver/luna/src/app/elements/asset-tree/asset-tree.component.ts +++ b/jumpserver/luna/src/app/elements/asset-tree/asset-tree.component.ts @@ -42,6 +42,7 @@ export class ElementAssetTreeComponent implements OnInit, OnDestroy { expandNodes: any; assetsTree: any; remoteAppsTree: any; + DBAppsTree: any; isShowRMenu = false; rightClickSelectNode: any; hasLoginTo = false; @@ -127,7 +128,20 @@ export class ElementAssetTreeComponent implements OnInit, OnDestroy { this.remoteAppsTree.destroy(); this.initRemoteAppsTree(); } - + + refreshDBAppsTree() { + this.DBAppsTree.destroy(); + this.initDBAppsTree(); + } + + onDBAppsTreeNodeClick(event, treeId, treeNode, clickFlag) { + if (treeNode.isParent) { + this.DBAppsTree.expandNode(treeNode); + } else { + this._http.getUserProfile().subscribe(); + this.connectAsset(treeNode); + } + } onRemoteAppsNodeClick(event, treeId, treeNode, clickFlag) { if (treeNode.isParent) { this.remoteAppsTree.expandNode(treeNode); @@ -156,17 +170,37 @@ export class ElementAssetTreeComponent implements OnInit, OnDestroy { } ); } + initDBAppsTree() { + const setting = Object.assign({}, this.setting); + setting['callback'] = { + onClick: this.onDBAppsTreeNodeClick.bind(this), + onRightClick: this.onRightClick.bind(this) + }; + this._http.getMyGrantedDBApps().subscribe( + resp => { + if (resp.length === 1) { + return; + } + const tree = $.fn.zTree.init($('#DBAppsTree'), setting, resp); + this.DBAppsTree = tree; + this.rootNodeAddDom(tree, () => { + this.refreshDBAppsTree(); + }); + } + ); + } initTree() { this.initAssetsTree(); this.initRemoteAppsTree(); + this.initDBAppsTree(); } connectAsset(node: TreeNode) { const evt = new ConnectEvt(node, 'asset'); connectEvt.next(evt); } - + rootNodeAddDom(ztree, callback) { const tId = ztree.setting.treeId + '_tree_refresh'; const refreshIcon = '' + diff --git a/jumpserver/luna/src/app/elements/connect/connect.component.ts b/jumpserver/luna/src/app/elements/connect/connect.component.ts index 66750b00921a6647a6e2bb7a4c6819fa5f30546b..7e8bafa78fe4b8f47c17078dc86c2a01f311a8e0 100644 --- a/jumpserver/luna/src/app/elements/connect/connect.component.ts +++ b/jumpserver/luna/src/app/elements/connect/connect.component.ts @@ -83,6 +83,9 @@ export class ElementConnectComponent implements OnInit, OnDestroy { case 'remote_app': this.connectRemoteApp(node); break; + case 'database_app': + this.connectDatabaseApp(node); + break; default: alert('Unknown type: ' + node.meta.type); } @@ -100,6 +103,18 @@ export class ElementConnectComponent implements OnInit, OnDestroy { } } + async connectDatabaseApp(node: TreeNode) { + this._logger.debug('Connect remote app: ', node.id); + const systemUsers = await this._http.getMyDatabaseAppSystemUsers(node.id).toPromise(); + let sysUser = await this.selectLoginSystemUsers(systemUsers); + sysUser = await this.manualSetUserAuthLoginIfNeed(sysUser); + if (sysUser && sysUser.id) { + this.loginDatabaseApp(node, sysUser); + } else { + alert('该主机没有授权系统用户'); + } + } + selectLoginSystemUsers(systemUsers: Array): Promise { const systemUserMaxPriority = this.filterMaxPrioritySystemUsers(systemUsers); let user: SystemUser; @@ -184,6 +199,20 @@ export class ElementConnectComponent implements OnInit, OnDestroy { this.onNewView.emit(view); } } + loginDatabaseApp(node: TreeNode, user: SystemUser) { + if (node) { + const view = new View(); + view.host = node; + view.nick = node.name; + view.connected = true; + view.editable = false; + view.closed = false; + view.DatabaseApp = node.id; + view.user = user; + view.type = 'database'; + this.onNewView.emit(view); + } + } connectFileManager(node: TreeNode) { const host = node.meta.asset as Asset; diff --git a/jumpserver/luna/src/app/elements/content-window/content-window.component.html b/jumpserver/luna/src/app/elements/content-window/content-window.component.html index b5bdff5ba2cb8af82e4a412f0f9c3a4cd451f66c..fb8115104f07885792b0f2fc941ac3cbae92c8b7 100644 --- a/jumpserver/luna/src/app/elements/content-window/content-window.component.html +++ b/jumpserver/luna/src/app/elements/content-window/content-window.component.html @@ -3,7 +3,7 @@ [view]="view" [host]="view.host" [sysUser]="view.user" - *ngIf="view.type=='ssh'" + *ngIf="view.type=='ssh' || view.type=='database'" > +
    + {{ idleTimeoutMsg | trans }} +
    diff --git a/jumpserver/luna/src/app/elements/guacamole/guacamole.component.scss b/jumpserver/luna/src/app/elements/guacamole/guacamole.component.scss index 39b8d649fe42e9c7af82762083af7fc009e26c8d..86263b0bb2e489f1da1c48f8a041bb6313dbff5a 100644 --- a/jumpserver/luna/src/app/elements/guacamole/guacamole.component.scss +++ b/jumpserver/luna/src/app/elements/guacamole/guacamole.component.scss @@ -6,3 +6,9 @@ iframe { .rdpIframe { height: 100%; } + +.idleTimeout { + padding: 50px; + color: red; + font-size: 30px; +} diff --git a/jumpserver/luna/src/app/elements/guacamole/guacamole.component.ts b/jumpserver/luna/src/app/elements/guacamole/guacamole.component.ts index 5e187e027225c5358e00efa1559d56e7d4ba3564..eb6dabca9089769a66d9b52d66a65e79e7f19360 100644 --- a/jumpserver/luna/src/app/elements/guacamole/guacamole.component.ts +++ b/jumpserver/luna/src/app/elements/guacamole/guacamole.component.ts @@ -1,6 +1,6 @@ import {Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core'; import {CookieService} from 'ngx-cookie-service'; -import {HttpService, LogService} from '@app/services'; +import {HttpService, LogService, SettingService} from '@app/services'; import {DataStore, User} from '@app/globals'; import {DomSanitizer} from '@angular/platform-browser'; import {View} from '@app/model'; @@ -19,16 +19,22 @@ export class ElementGuacamoleComponent implements OnInit { @Input() index: number; @ViewChild('rdpRef') el: ElementRef; registered = false; + iframeWindow: any; + idleTimeout: number; + idleTTL = 1000 * 3600; + isIdleTimeout = false; + idleTimeoutMsg = 'Idle timeout, connection has been disconnected'; constructor(private sanitizer: DomSanitizer, private _http: HttpService, private _cookie: CookieService, + private settingSvc: SettingService, private _logger: LogService) { + this.idleTTL = this.settingSvc.globalSetting.SECURITY_MAX_IDLE_TIME * 60 * 1000; } registerHost() { let action: any; - console.log(this.sysUser); if (this.remoteAppId) { action = this._http.guacamoleAddRemoteApp(User.id, this.remoteAppId, this.sysUser.id, this.sysUser.username, this.sysUser.password); } else { @@ -38,6 +44,7 @@ export class ElementGuacamoleComponent implements OnInit { data => { const base = data.result; this.target = document.location.origin + '/guacamole/#/client/' + base + '?token=' + DataStore.guacamoleToken; + setTimeout(() => this.setIdleTimeout(), 500); }, error => { if (!this.registered) { @@ -73,15 +80,31 @@ export class ElementGuacamoleComponent implements OnInit { if (this.target) { return null; } - - // if (!environment.production) { - // this.target = this._cookie.get('guacamole'); - // NavList.List[this.index].Rdp = this.el.nativeElement; - // return null; - // } this.registerHost(); } + setIdleTimeout() { + this.iframeWindow = this.el.nativeElement.contentWindow; + this.resetIdleTimeout(); + this.iframeWindow.onclick = () => this.resetIdleTimeout(); + this.iframeWindow.onkeyup = () => this.resetIdleTimeout(); + console.log(this.iframeWindow); + } + + resetIdleTimeout() { + if (this.idleTimeout) { + clearTimeout(this.idleTimeout); + this.idleTimeout = null; + } + this.idleTimeout = setTimeout(() => this.disconnect(), this.idleTTL); + } + + disconnect() { + this._logger.debug('Disconnect guacamole'); + this.target = ''; + this.isIdleTimeout = true; + } + trust(url) { return this.sanitizer.bypassSecurityTrustResourceUrl(url); } diff --git a/jumpserver/luna/src/app/elements/ssh-term/ssh-term.component.ts b/jumpserver/luna/src/app/elements/ssh-term/ssh-term.component.ts index c1be637d9f07a533388cddc9ec6f565404402576..a6d34e26f7fdea63d3225363e63af3aa089dcf42 100644 --- a/jumpserver/luna/src/app/elements/ssh-term/ssh-term.component.ts +++ b/jumpserver/luna/src/app/elements/ssh-term/ssh-term.component.ts @@ -34,7 +34,7 @@ export class ElementSshTermComponent implements OnInit, OnDestroy { this.ws = sock; this.connectHost(); }); - this.view.type = 'ssh'; + // this.view.type = 'ssh'; } newTerm() { @@ -74,7 +74,8 @@ export class ElementSshTermComponent implements OnInit, OnDestroy { uuid: this.host.id, userid: this.sysUser.id, secret: this.secret, - size: [this.term.cols, this.term.rows] + size: [this.term.cols, this.term.rows], + type: this.view.type }; this.ws.emit('host', data); } diff --git a/jumpserver/luna/src/app/model.ts b/jumpserver/luna/src/app/model.ts index 9318b882c13bc3002d359f089ba8ad20cfb6d089..bcd5c80a6ec232ed4622079a1f0f237f102f367f 100644 --- a/jumpserver/luna/src/app/model.ts +++ b/jumpserver/luna/src/app/model.ts @@ -116,6 +116,7 @@ export class View { room: string; Rdp: any; Term: any; + DatabaseApp: string; } export class ViewAction { @@ -188,6 +189,7 @@ export class Monitor { export class GlobalSetting { WINDOWS_SKIP_ALL_MANUAL_PASSWORD: boolean; + SECURITY_MAX_IDLE_TIME: number; } export class Setting { diff --git a/jumpserver/luna/src/app/services/http.ts b/jumpserver/luna/src/app/services/http.ts index afa2307834da64a020e3a676fde928b4236b0892..14125916d45a21c9176239b9ca566dfd3be6920b 100644 --- a/jumpserver/luna/src/app/services/http.ts +++ b/jumpserver/luna/src/app/services/http.ts @@ -99,11 +99,24 @@ export class HttpService { return this.http.get>(url); } + getMyGrantedDBApps(id?: string) { + let url = '/api/v1/perms/user/database-apps/tree/'; + if (id) { + url += `?id=${id}&only=1`; + } + return this.http.get>(url); + } + getMyRemoteAppSystemUsers(remoteAppId: string) { const url = `/api/v1/perms/users/remote-apps/${remoteAppId}/system-users/`; return this.http.get>(url); } + getMyDatabaseAppSystemUsers(DatabaseAppId: string) { + const url = `/api/v1/perms/users/database-apps/${DatabaseAppId}/system-users/`; + return this.http.get>(url); + } + getMyAssetSystemUsers(assetId: string) { const url = `/api/v1/perms/users/assets/${assetId}/system-users/`; return this.http.get>(url); @@ -122,12 +135,10 @@ export class HttpService { return this.delete(url); } } - getFavoriteAssets() { const url = '/api/v1/assets/favorite-assets/'; return this.http.get>(url); } - getGuacamoleToken(user_id: string, authToken: string) { const body = new HttpParams() .set('username', user_id) diff --git a/jumpserver/luna/src/assets/i18n/zh.json b/jumpserver/luna/src/assets/i18n/zh.json index e3cc526ce63397ea72e2670da689d583b2cba937..86ef0d8b2b4be71cccec40cc303b076b17dd7ee6 100644 --- a/jumpserver/luna/src/assets/i18n/zh.json +++ b/jumpserver/luna/src/assets/i18n/zh.json @@ -79,5 +79,6 @@ "rows": "行数", "favorite": "收藏", "disfavor": "取消收藏", - "success": "成功" + "success": "成功", + "idle timeout, connection has been disconnected": "空闲超时,链接已断开" } diff --git a/jumpserver/luna/src/environments/environment.prod.ts b/jumpserver/luna/src/environments/environment.prod.ts index cc12dfe75a62d5b95f20db3e3504e9a1ed1bd45c..a28b33ab642b0d2822a50d6b3d1d7e1ffd7346ec 100644 --- a/jumpserver/luna/src/environments/environment.prod.ts +++ b/jumpserver/luna/src/environments/environment.prod.ts @@ -2,5 +2,5 @@ export const environment = { production: true }; // export const version = '1.3.0-{{BUILD_NUMBER}} GPLv2.'; -export const version = '1.5.4-101 GPLv2.'; +export const version = '1.5.6-101 GPLv2.'; // export const version = '1.4.1-{{BUILD_NUMBER}} GPLv2.';