diff --git a/.gitignore b/.gitignore index cb931287b..5d5eb57db 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ dump.rdb .tox .cache/ .idea/ +.vscode/ db.sqlite3 config.py config.yml diff --git a/README.md b/README.md index 42a1f7fad..58ba7899c 100644 --- a/README.md +++ b/README.md @@ -265,6 +265,7 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向 - [极速安装](https://docs.jumpserver.org/zh/master/install/setup_by_fast/) - [完整文档](https://docs.jumpserver.org) - [演示视频](https://www.bilibili.com/video/BV1ZV41127GB) +- [手动安装](https://github.com/jumpserver/installer) ## 组件项目 - [Lina](https://github.com/jumpserver/lina) JumpServer Web UI 项目 diff --git a/apps/applications/api/application.py b/apps/applications/api/application.py index cb51ff023..84426b6b2 100644 --- a/apps/applications/api/application.py +++ b/apps/applications/api/application.py @@ -2,18 +2,49 @@ # from orgs.mixins.api import OrgBulkModelViewSet +from rest_framework import generics -from ..hands import IsOrgAdminOrAppUser +from ..hands import IsOrgAdminOrAppUser, IsOrgAdmin from .. import models, serializers +from ..models import Application +from assets.models import SystemUser +from assets.serializers import SystemUserListSerializer +from perms.models import ApplicationPermission +from ..const import ApplicationCategoryChoices -__all__ = ['ApplicationViewSet'] +__all__ = ['ApplicationViewSet', 'ApplicationUserListApi'] class ApplicationViewSet(OrgBulkModelViewSet): - model = models.Application + model = Application filterset_fields = ('name', 'type', 'category') search_fields = filterset_fields permission_classes = (IsOrgAdminOrAppUser,) serializer_class = serializers.ApplicationSerializer + +class ApplicationUserListApi(generics.ListAPIView): + permission_classes = (IsOrgAdmin, ) + filterset_fields = ('name', 'username') + search_fields = filterset_fields + serializer_class = SystemUserListSerializer + + def get_application(self): + application = None + app_id = self.request.query_params.get('application_id') + if app_id: + application = Application.objects.get(id=app_id) + return application + + def get_queryset(self): + queryset = SystemUser.objects.none() + application = self.get_application() + if not application: + return queryset + system_user_ids = ApplicationPermission.objects.filter(applications=application)\ + .values_list('system_users', flat=True) + if not system_user_ids: + return queryset + queryset = SystemUser.objects.filter(id__in=system_user_ids) + return queryset diff --git a/apps/applications/urls/api_urls.py b/apps/applications/urls/api_urls.py index ab463a401..9ca50d32c 100644 --- a/apps/applications/urls/api_urls.py +++ b/apps/applications/urls/api_urls.py @@ -14,6 +14,7 @@ router.register(r'applications', api.ApplicationViewSet, 'application') urlpatterns = [ path('remote-apps//connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'), + path('application-users/', api.ApplicationUserListApi.as_view(), name='application-user') ] diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index 8ec151285..25c3f58f4 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -3,14 +3,13 @@ from django.shortcuts import get_object_or_404 from rest_framework.response import Response from common.utils import get_logger -from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser -from common.drf.filters import CustomFilter +from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsValidUser from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins import generics -from orgs.utils import tmp_to_org +from orgs.utils import tmp_to_root_org from ..models import SystemUser, Asset from .. import serializers -from ..serializers import SystemUserWithAuthInfoSerializer +from ..serializers import SystemUserWithAuthInfoSerializer, SystemUserTempAuthSerializer from ..tasks import ( push_system_user_to_assets_manual, test_system_user_connectivity_manual, push_system_user_to_assets @@ -21,6 +20,7 @@ logger = get_logger(__file__) __all__ = [ 'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi', 'SystemUserCommandFilterRuleListApi', 'SystemUserTaskApi', 'SystemUserAssetsListView', + 'SystemUserTempAuthInfoApi', 'SystemUserAppAuthInfoApi', ] @@ -57,6 +57,25 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView): return Response(status=204) +class SystemUserTempAuthInfoApi(generics.CreateAPIView): + model = SystemUser + permission_classes = (IsValidUser,) + serializer_class = SystemUserTempAuthSerializer + + def create(self, request, *args, **kwargs): + serializer = super().get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + pk = kwargs.get('pk') + user = self.request.user + data = serializer.validated_data + instance_id = data.get('instance_id') + + with tmp_to_root_org(): + instance = get_object_or_404(SystemUser, pk=pk) + instance.set_temp_auth(instance_id, user, data) + return Response(serializer.data, status=201) + + class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView): """ Get system user with asset auth info @@ -65,22 +84,30 @@ class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView): permission_classes = (IsOrgAdminOrAppUser,) serializer_class = SystemUserWithAuthInfoSerializer - def get_exception_handler(self): - def handler(e, context): - return Response({"error": str(e)}, status=400) - return handler + def get_object(self): + instance = super().get_object() + asset_id = self.kwargs.get('asset_id') + user_id = self.request.query_params.get("user_id") + username = self.request.query_params.get("username") + instance.load_asset_more_auth(asset_id=asset_id, user_id=user_id, username=username) + return instance + + +class SystemUserAppAuthInfoApi(generics.RetrieveAPIView): + """ + Get system user with asset auth info + """ + model = SystemUser + permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = SystemUserWithAuthInfoSerializer def get_object(self): instance = super().get_object() - username = instance.username - if instance.username_same_with_user: - username = self.request.query_params.get("username") - asset_id = self.kwargs.get('aid') - asset = get_object_or_404(Asset, pk=asset_id) - - with tmp_to_org(asset.org_id): - instance.load_asset_special_auth(asset=asset, username=username) - return instance + app_id = self.kwargs.get('app_id') + user_id = self.request.query_params.get("user_id") + if user_id: + instance.load_app_more_auth(app_id, user_id) + return instance class SystemUserTaskApi(generics.CreateAPIView): diff --git a/apps/assets/backends/base.py b/apps/assets/backends/base.py index 3b27a57af..17115afaa 100644 --- a/apps/assets/backends/base.py +++ b/apps/assets/backends/base.py @@ -31,11 +31,11 @@ class BaseBackend: def qs_to_values(qs): values = qs.values( 'hostname', 'ip', "asset_id", - 'username', 'password', 'private_key', 'public_key', + 'name', 'username', 'password', 'private_key', 'public_key', 'score', 'version', "asset_username", "union_id", 'date_created', 'date_updated', - 'org_id', 'backend', + 'org_id', 'backend', 'backend_display' ) return values diff --git a/apps/assets/backends/db.py b/apps/assets/backends/db.py index 386f0ee29..aa3e1ef78 100644 --- a/apps/assets/backends/db.py +++ b/apps/assets/backends/db.py @@ -106,6 +106,7 @@ class DBBackend(BaseBackend): class SystemUserBackend(DBBackend): model = SystemUser.assets.through backend = 'system_user' + backend_display = _('System user') prefer = backend base_score = 0 union_id_length = 2 @@ -138,6 +139,7 @@ class SystemUserBackend(DBBackend): kwargs = dict( hostname=F("asset__hostname"), ip=F("asset__ip"), + name=F("systemuser__name"), username=F("systemuser__username"), password=F("systemuser__password"), private_key=F("systemuser__private_key"), @@ -152,7 +154,8 @@ class SystemUserBackend(DBBackend): union_id=Concat(F("systemuser_id"), Value("_"), F("asset_id"), output_field=CharField()), org_id=F("asset__org_id"), - backend=Value(self.backend, CharField()) + backend=Value(self.backend, CharField()), + backend_display=Value(self.backend_display, CharField()), ) return kwargs @@ -174,12 +177,17 @@ class SystemUserBackend(DBBackend): class DynamicSystemUserBackend(SystemUserBackend): backend = 'system_user_dynamic' + backend_display = _('System user(Dynamic)') prefer = 'system_user' union_id_length = 3 def get_annotate(self): kwargs = super().get_annotate() kwargs.update(dict( + name=Concat( + F("systemuser__users__name"), Value('('), F("systemuser__name"), Value(')'), + output_field=CharField() + ), username=F("systemuser__users__username"), asset_username=Concat( F("asset__id"), Value("_"), @@ -221,6 +229,7 @@ class DynamicSystemUserBackend(SystemUserBackend): class AdminUserBackend(DBBackend): model = Asset backend = 'admin_user' + backend_display = _('Admin user') prefer = backend base_score = 200 @@ -246,6 +255,7 @@ class AdminUserBackend(DBBackend): def all(self): qs = self.model.objects.all().annotate( asset_id=F("id"), + name=F("admin_user__name"), username=F("admin_user__username"), password=F("admin_user__password"), private_key=F("admin_user__private_key"), @@ -256,6 +266,7 @@ class AdminUserBackend(DBBackend): asset_username=Concat(F("id"), Value("_"), F("admin_user__username"), output_field=CharField()), union_id=Concat(F("admin_user_id"), Value("_"), F("id"), output_field=CharField()), backend=Value(self.backend, CharField()), + backend_display=Value(self.backend_display, CharField()), ) qs = self.qs_to_values(qs) return qs @@ -264,6 +275,7 @@ class AdminUserBackend(DBBackend): class AuthbookBackend(DBBackend): model = AuthBook backend = 'db' + backend_display = _('Database') prefer = backend base_score = 400 @@ -313,6 +325,7 @@ class AuthbookBackend(DBBackend): asset_username=Concat(F("asset__id"), Value("_"), F("username"), output_field=CharField()), union_id=Concat(F("id"), Value("_"), F("asset_id"), output_field=CharField()), backend=Value(self.backend, CharField()), + backend_display=Value(self.backend_display, CharField()), ) qs = self.qs_to_values(qs) return qs diff --git a/apps/assets/migrations/0002_auto_20180105_1807.py b/apps/assets/migrations/0002_auto_20180105_1807.py new file mode 100644 index 000000000..bf1f022ac --- /dev/null +++ b/apps/assets/migrations/0002_auto_20180105_1807.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-01-05 10:07 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='adminuser', + options={'ordering': ['name'], 'verbose_name': 'Admin user'}, + ), + migrations.AlterModelOptions( + name='asset', + options={'verbose_name': 'Asset'}, + ), + migrations.AlterModelOptions( + name='assetgroup', + options={'ordering': ['name'], 'verbose_name': 'Asset group'}, + ), + migrations.AlterModelOptions( + name='cluster', + options={'ordering': ['name'], 'verbose_name': 'Cluster'}, + ), + migrations.AlterModelOptions( + name='systemuser', + options={'ordering': ['name'], 'verbose_name': 'System user'}, + ), + ] diff --git a/apps/assets/migrations/0003_auto_20180109_2331.py b/apps/assets/migrations/0003_auto_20180109_2331.py new file mode 100644 index 000000000..254de6236 --- /dev/null +++ b/apps/assets/migrations/0003_auto_20180109_2331.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-01-09 15:31 +from __future__ import unicode_literals + +import assets.models.asset +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0002_auto_20180105_1807'), + ] + + operations = [ + migrations.AlterField( + model_name='asset', + name='cluster', + field=models.ForeignKey(default=assets.models.asset.default_cluster, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='assets', to='assets.Cluster', verbose_name='Cluster'), + ), + ] diff --git a/apps/assets/migrations/0004_auto_20180125_1218.py b/apps/assets/migrations/0004_auto_20180125_1218.py new file mode 100644 index 000000000..1886fa499 --- /dev/null +++ b/apps/assets/migrations/0004_auto_20180125_1218.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-01-25 04:18 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0003_auto_20180109_2331'), + ] + + operations = [ + migrations.AlterField( + model_name='assetgroup', + name='created_by', + field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by'), + ), + ] diff --git a/apps/assets/migrations/0005_auto_20180126_1637.py b/apps/assets/migrations/0005_auto_20180126_1637.py new file mode 100644 index 000000000..8db19e482 --- /dev/null +++ b/apps/assets/migrations/0005_auto_20180126_1637.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-01-26 08:37 +from __future__ import unicode_literals + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0004_auto_20180125_1218'), + ] + + operations = [ + migrations.CreateModel( + name='Label', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('value', models.CharField(max_length=128, verbose_name='Value')), + ('category', models.CharField(choices=[('S', 'System'), ('U', 'User')], default='U', max_length=128, verbose_name='Category')), + ('is_active', models.BooleanField(default=True, verbose_name='Is active')), + ('comment', models.TextField(blank=True, null=True, verbose_name='Comment')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ], + options={ + 'db_table': 'assets_label', + }, + ), + migrations.AlterUniqueTogether( + name='label', + unique_together=set([('name', 'value')]), + ), + migrations.AddField( + model_name='asset', + name='labels', + field=models.ManyToManyField(blank=True, related_name='assets', to='assets.Label', verbose_name='Labels'), + ), + ] diff --git a/apps/assets/migrations/0006_auto_20180130_1502.py b/apps/assets/migrations/0006_auto_20180130_1502.py new file mode 100644 index 000000000..b77470d27 --- /dev/null +++ b/apps/assets/migrations/0006_auto_20180130_1502.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-01-30 07:02 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0005_auto_20180126_1637'), + ] + + operations = [ + migrations.RemoveField( + model_name='asset', + name='cabinet_no', + ), + migrations.RemoveField( + model_name='asset', + name='cabinet_pos', + ), + migrations.RemoveField( + model_name='asset', + name='env', + ), + migrations.RemoveField( + model_name='asset', + name='remote_card_ip', + ), + migrations.RemoveField( + model_name='asset', + name='status', + ), + migrations.RemoveField( + model_name='asset', + name='type', + ), + ] diff --git a/apps/assets/migrations/0007_auto_20180225_1815.py b/apps/assets/migrations/0007_auto_20180225_1815.py new file mode 100644 index 000000000..009381bcb --- /dev/null +++ b/apps/assets/migrations/0007_auto_20180225_1815.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-02-25 10:15 +from __future__ import unicode_literals + +import assets.models.asset +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0006_auto_20180130_1502'), + ] + + operations = [ + migrations.CreateModel( + name='Node', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('key', models.CharField(max_length=64, unique=True, verbose_name='Key')), + ('value', models.CharField(max_length=128, unique=True, verbose_name='Value')), + ('child_mark', models.IntegerField(default=0)), + ('date_create', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.RemoveField( + model_name='asset', + name='cluster', + ), + migrations.RemoveField( + model_name='asset', + name='groups', + ), + migrations.RemoveField( + model_name='systemuser', + name='cluster', + ), + migrations.AlterField( + model_name='asset', + name='admin_user', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='assets.AdminUser', verbose_name='Admin user'), + ), + migrations.AlterField( + model_name='systemuser', + name='protocol', + field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp')], default='ssh', max_length=16, verbose_name='Protocol'), + ), + migrations.AddField( + model_name='asset', + name='nodes', + field=models.ManyToManyField(default=assets.models.asset.default_node, related_name='assets', to='assets.Node', verbose_name='Nodes'), + ), + migrations.AddField( + model_name='systemuser', + name='nodes', + field=models.ManyToManyField(blank=True, to='assets.Node', verbose_name='Nodes'), + ), + ] diff --git a/apps/assets/migrations/0008_auto_20180306_1804.py b/apps/assets/migrations/0008_auto_20180306_1804.py new file mode 100644 index 000000000..48d352619 --- /dev/null +++ b/apps/assets/migrations/0008_auto_20180306_1804.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-03-06 10:04 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0007_auto_20180225_1815'), + ] + + operations = [ + migrations.AlterField( + model_name='adminuser', + name='created_by', + field=models.CharField(max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='adminuser', + name='username', + field=models.CharField(max_length=128, verbose_name='Username'), + ), + migrations.AlterField( + model_name='asset', + name='platform', + field=models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Other', 'Other')], default='Linux', max_length=128, verbose_name='Platform'), + ), + migrations.AlterField( + model_name='systemuser', + name='created_by', + field=models.CharField(max_length=128, null=True, verbose_name='Created by'), + ), + migrations.AlterField( + model_name='systemuser', + name='username', + field=models.CharField(max_length=128, verbose_name='Username'), + ), + ] diff --git a/apps/assets/migrations/0009_auto_20180307_1212.py b/apps/assets/migrations/0009_auto_20180307_1212.py new file mode 100644 index 000000000..08d770642 --- /dev/null +++ b/apps/assets/migrations/0009_auto_20180307_1212.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-03-07 04:12 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0008_auto_20180306_1804'), + ] + + operations = [ + migrations.AlterField( + model_name='node', + name='value', + field=models.CharField(max_length=128, verbose_name='Value'), + ), + ] diff --git a/apps/assets/migrations/0010_auto_20180307_1749.py b/apps/assets/migrations/0010_auto_20180307_1749.py new file mode 100644 index 000000000..5e6be0943 --- /dev/null +++ b/apps/assets/migrations/0010_auto_20180307_1749.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-03-07 09:49 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0009_auto_20180307_1212'), + ] + + operations = [ + migrations.AlterField( + model_name='node', + name='value', + field=models.CharField(max_length=128, unique=True, verbose_name='Value'), + ), + ] diff --git a/apps/assets/migrations/0011_auto_20180326_0957.py b/apps/assets/migrations/0011_auto_20180326_0957.py new file mode 100644 index 000000000..07b9055dc --- /dev/null +++ b/apps/assets/migrations/0011_auto_20180326_0957.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-03-26 01:57 +from __future__ import unicode_literals + +import assets.models.utils +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0010_auto_20180307_1749'), + ] + + operations = [ + migrations.CreateModel( + name='Domain', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=128, unique=True, verbose_name='Name')), + ('comment', models.TextField(blank=True, verbose_name='Comment')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ], + ), + migrations.CreateModel( + name='Gateway', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=128, unique=True, verbose_name='Name')), + ('username', models.CharField(max_length=128, verbose_name='Username')), + ('_password', models.CharField(blank=True, max_length=256, null=True, verbose_name='Password')), + ('_private_key', models.TextField(blank=True, max_length=4096, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key')), + ('_public_key', models.TextField(blank=True, max_length=4096, verbose_name='SSH public key')), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('date_updated', models.DateTimeField(auto_now=True)), + ('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')), + ('ip', models.GenericIPAddressField(db_index=True, verbose_name='IP')), + ('port', models.IntegerField(default=22, verbose_name='Port')), + ('protocol', models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp')], default='ssh', max_length=16, verbose_name='Protocol')), + ('comment', models.CharField(blank=True, max_length=128, null=True, verbose_name='Comment')), + ('is_active', models.BooleanField(default=True, verbose_name='Is active')), + ('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Domain', verbose_name='Domain')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='asset', + name='domain', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assets', to='assets.Domain', verbose_name='Domain'), + ), + ] diff --git a/apps/assets/migrations/0012_auto_20180404_1302.py b/apps/assets/migrations/0012_auto_20180404_1302.py new file mode 100644 index 000000000..0ccb63e27 --- /dev/null +++ b/apps/assets/migrations/0012_auto_20180404_1302.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-04-04 05:02 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0011_auto_20180326_0957'), + ] + + operations = [ + migrations.AlterField( + model_name='asset', + name='domain', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assets', to='assets.Domain', verbose_name='Domain'), + ), + ] diff --git a/apps/assets/migrations/0013_auto_20180411_1135.py b/apps/assets/migrations/0013_auto_20180411_1135.py new file mode 100644 index 000000000..baaf789bd --- /dev/null +++ b/apps/assets/migrations/0013_auto_20180411_1135.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-04-11 03:35 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0012_auto_20180404_1302'), + ] + + operations = [ + migrations.AddField( + model_name='systemuser', + name='assets', + field=models.ManyToManyField(blank=True, to='assets.Asset', verbose_name='Assets'), + ), + migrations.AlterField( + model_name='systemuser', + name='sudo', + field=models.TextField(default='/bin/whoami', verbose_name='Sudo'), + ), + ] diff --git a/apps/assets/migrations/0014_auto_20180427_1245.py b/apps/assets/migrations/0014_auto_20180427_1245.py new file mode 100644 index 000000000..735a50879 --- /dev/null +++ b/apps/assets/migrations/0014_auto_20180427_1245.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-04-27 04:45 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0013_auto_20180411_1135'), + ] + + operations = [ + migrations.AlterField( + model_name='adminuser', + name='username', + field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'), + ), + migrations.AlterField( + model_name='gateway', + name='username', + field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'), + ), + migrations.AlterField( + model_name='systemuser', + name='username', + field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'), + ), + ] diff --git a/apps/assets/migrations/0015_auto_20180510_1235.py b/apps/assets/migrations/0015_auto_20180510_1235.py new file mode 100644 index 000000000..81d12d2e4 --- /dev/null +++ b/apps/assets/migrations/0015_auto_20180510_1235.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-05-10 04:35 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0014_auto_20180427_1245'), + ] + + operations = [ + migrations.AlterField( + model_name='adminuser', + name='username', + field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'), + ), + migrations.AlterField( + model_name='gateway', + name='username', + field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'), + ), + migrations.AlterField( + model_name='systemuser', + name='username', + field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'), + ), + ] diff --git a/apps/assets/migrations/0016_auto_20180511_1203.py b/apps/assets/migrations/0016_auto_20180511_1203.py new file mode 100644 index 000000000..32f79a3c6 --- /dev/null +++ b/apps/assets/migrations/0016_auto_20180511_1203.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-05-11 04:03 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0015_auto_20180510_1235'), + ] + + operations = [ + migrations.AlterField( + model_name='node', + name='value', + field=models.CharField(max_length=128, verbose_name='Value'), + ), + ] diff --git a/apps/assets/migrations/0017_auto_20180702_1415.py b/apps/assets/migrations/0017_auto_20180702_1415.py new file mode 100644 index 000000000..9950424a6 --- /dev/null +++ b/apps/assets/migrations/0017_auto_20180702_1415.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-07-02 06:15 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +def migrate_win_to_ssh_protocol(apps, schema_editor): + asset_model = apps.get_model("assets", "Asset") + db_alias = schema_editor.connection.alias + asset_model.objects.using(db_alias).filter(platform__startswith='Win').update(protocol='rdp') + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0016_auto_20180511_1203'), + ] + + operations = [ + migrations.AddField( + model_name='asset', + name='protocol', + field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)')], default='ssh', max_length=128, verbose_name='Protocol'), + ), + migrations.AddField( + model_name='systemuser', + name='login_mode', + field=models.CharField(choices=[('auto', 'Automatic login'), ('manual', 'Manually login')], default='auto', max_length=10, verbose_name='Login mode'), + ), + migrations.AlterField( + model_name='adminuser', + name='username', + field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'), + ), + migrations.AlterField( + model_name='asset', + name='platform', + field=models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Windows2016', 'Windows(2016)'), ('Other', 'Other')], default='Linux', max_length=128, verbose_name='Platform'), + ), + migrations.AlterField( + model_name='gateway', + name='username', + field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'), + ), + migrations.AlterField( + model_name='systemuser', + name='protocol', + field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)')], default='ssh', max_length=16, verbose_name='Protocol'), + ), + migrations.AlterField( + model_name='systemuser', + name='username', + field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'), + ), + migrations.RunPython(migrate_win_to_ssh_protocol), + ] diff --git a/apps/assets/migrations/0018_auto_20180807_1116.py b/apps/assets/migrations/0018_auto_20180807_1116.py new file mode 100644 index 000000000..c4e848b43 --- /dev/null +++ b/apps/assets/migrations/0018_auto_20180807_1116.py @@ -0,0 +1,84 @@ +# Generated by Django 2.0.7 on 2018-08-07 03:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0017_auto_20180702_1415'), + ] + + operations = [ + migrations.AddField( + model_name='adminuser', + name='org_id', + field=models.CharField(blank=True, default=None, max_length=36, null=True), + ), + migrations.AddField( + model_name='asset', + name='org_id', + field=models.CharField(blank=True, default=None, max_length=36, null=True), + ), + migrations.AddField( + model_name='domain', + name='org_id', + field=models.CharField(blank=True, default=None, max_length=36, null=True), + ), + migrations.AddField( + model_name='gateway', + name='org_id', + field=models.CharField(blank=True, default=None, max_length=36, null=True), + ), + migrations.AddField( + model_name='label', + name='org_id', + field=models.CharField(blank=True, default=None, max_length=36, null=True), + ), + migrations.AddField( + model_name='node', + name='org_id', + field=models.CharField(blank=True, default=None, max_length=36, null=True), + ), + migrations.AddField( + model_name='systemuser', + name='org_id', + field=models.CharField(blank=True, default=None, max_length=36, null=True), + ), + migrations.AlterField( + model_name='adminuser', + name='name', + field=models.CharField(max_length=128, verbose_name='Name'), + ), + migrations.AlterField( + model_name='asset', + name='hostname', + field=models.CharField(max_length=128, verbose_name='Hostname'), + ), + migrations.AlterField( + model_name='gateway', + name='name', + field=models.CharField(max_length=128, verbose_name='Name'), + ), + migrations.AlterField( + model_name='systemuser', + name='name', + field=models.CharField(max_length=128, verbose_name='Name'), + ), + migrations.AlterUniqueTogether( + name='adminuser', + unique_together={('name', 'org_id')}, + ), + migrations.AlterUniqueTogether( + name='asset', + unique_together={('org_id', 'hostname')}, + ), + migrations.AlterUniqueTogether( + name='gateway', + unique_together={('name', 'org_id')}, + ), + migrations.AlterUniqueTogether( + name='systemuser', + unique_together={('name', 'org_id')}, + ), + ] diff --git a/apps/assets/migrations/0019_auto_20180816_1320.py b/apps/assets/migrations/0019_auto_20180816_1320.py new file mode 100644 index 000000000..0d468e511 --- /dev/null +++ b/apps/assets/migrations/0019_auto_20180816_1320.py @@ -0,0 +1,22 @@ +# Generated by Django 2.0.7 on 2018-08-16 05:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0018_auto_20180807_1116'), + ] + + operations = [ + migrations.AddField( + model_name='asset', + name='cpu_vcpus', + field=models.IntegerField(null=True, verbose_name='CPU vcpus'), + ), + migrations.AlterUniqueTogether( + name='label', + unique_together={('name', 'value', 'org_id')}, + ), + ] diff --git a/apps/assets/models/asset_user.py b/apps/assets/models/asset_user.py index 118d4549b..ac9112427 100644 --- a/apps/assets/models/asset_user.py +++ b/apps/assets/models/asset_user.py @@ -7,6 +7,7 @@ class AssetUser(AuthBook): hostname = "" ip = "" backend = "" + backend_display = "" union_id = "" asset_username = "" diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index 5ed16741f..6623cc6a7 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -11,8 +11,7 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from django.conf import settings -from common.db.models import ChoiceSet -from common.utils import random_string +from common.utils import random_string, signer from common.utils import ( ssh_key_string_to_obj, ssh_key_gen, get_logger, lazyproperty ) diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 37af8ea86..b64aeb758 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -7,9 +7,10 @@ import logging from django.db import models from django.utils.translation import ugettext_lazy as _ from django.core.validators import MinValueValidator, MaxValueValidator +from django.core.cache import cache -from common.utils import signer -from common.fields.model import JsonListCharField +from common.utils import signer, get_object_or_none +from common.exceptions import JMSException from .base import BaseUser from .asset import Asset @@ -185,6 +186,81 @@ class SystemUser(BaseUser): if self.username_same_with_user: self.username = other.username + def set_temp_auth(self, asset_or_app_id, user_id, auth, ttl=300): + if not auth: + raise ValueError('Auth not set') + key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id) + logger.debug(f'Set system user temp auth: {key}') + cache.set(key, auth, ttl) + + def get_temp_auth(self, asset_or_app_id, user_id): + key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id) + logger.debug(f'Get system user temp auth: {key}') + password = cache.get(key) + return password + + def load_tmp_auth_if_has(self, asset_or_app_id, user): + if not asset_or_app_id or not user: + return + if self.login_mode != self.LOGIN_MANUAL: + pass + + auth = self.get_temp_auth(asset_or_app_id, user) + if not auth: + return + username = auth.get('username') + password = auth.get('password') + + if username: + self.username = username + if password: + self.password = password + + def load_app_more_auth(self, app_id=None, user_id=None): + from users.models import User + + if self.login_mode == self.LOGIN_MANUAL: + self.password = '' + self.private_key = '' + if not user_id: + return + user = get_object_or_none(User, pk=user_id) + if not user: + return + self.load_tmp_auth_if_has(app_id, user) + + def load_asset_more_auth(self, asset_id=None, username=None, user_id=None): + from users.models import User + + if self.login_mode == self.LOGIN_MANUAL: + self.password = '' + self.private_key = '' + + asset = None + if asset_id: + asset = get_object_or_none(Asset, pk=asset_id) + # 没有资产就没有必要继续了 + if not asset: + logger.debug('Asset not found, pass') + return + + user = None + if user_id: + user = get_object_or_none(User, pk=user_id) + + if self.username_same_with_user: + if user and not username: + username = user.username + + # 加载某个资产的特殊配置认证信息 + try: + self.load_asset_special_auth(asset, username) + except Exception as e: + logger.error('Load special auth Error: ', e) + pass + + self.load_tmp_auth_if_has(asset_id, user) + @property def cmd_filter_rules(self): from .cmd_filter import CommandFilterRule diff --git a/apps/assets/serializers/asset_user.py b/apps/assets/serializers/asset_user.py index 19cb2adc7..cd098537a 100644 --- a/apps/assets/serializers/asset_user.py +++ b/apps/assets/serializers/asset_user.py @@ -47,22 +47,24 @@ class AssetUserReadSerializer(AssetUserWriteSerializer): ip = serializers.CharField(read_only=True, label=_("IP")) asset = serializers.CharField(source='asset_id', label=_('Asset')) backend = serializers.CharField(read_only=True, label=_("Backend")) + backend_display = serializers.CharField(read_only=True, label=_("Source")) class Meta(AssetUserWriteSerializer.Meta): read_only_fields = ( 'date_created', 'date_updated', 'created_by', 'version', ) - fields_mini = ['id', 'username'] + fields_mini = ['id', 'name', 'username'] fields_write_only = ['password', 'private_key', "public_key"] fields_small = fields_mini + fields_write_only + [ - 'backend', 'version', + 'backend', 'backend_display', 'version', 'date_created', "date_updated", 'comment' ] fields_fk = ['asset', 'hostname', 'ip'] fields = fields_small + fields_fk extra_kwargs = { + 'name': {'required': False}, 'username': {'required': True}, 'password': {'write_only': True}, 'private_key': {'write_only': True}, diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index 7f5befaed..726f53e34 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -14,6 +14,7 @@ __all__ = [ 'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer', 'SystemUserNodeRelationSerializer', 'SystemUserTaskSerializer', 'SystemUserUserRelationSerializer', 'SystemUserWithAuthInfoSerializer', + 'SystemUserTempAuthSerializer', ] @@ -272,3 +273,10 @@ class SystemUserTaskSerializer(serializers.Serializer): many=True ) task = serializers.CharField(read_only=True) + + +class SystemUserTempAuthSerializer(SystemUserSerializer): + instance_id = serializers.CharField() + + class Meta(SystemUserSerializer.Meta): + fields = ['instance_id', 'username', 'password'] diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index c8413f83e..8bc20a162 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -46,7 +46,9 @@ urlpatterns = [ path('system-users//auth-info/', api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'), path('system-users//assets/', api.SystemUserAssetsListView.as_view(), name='system-user-assets'), - path('system-users//assets//auth-info/', api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'), + path('system-users//assets//auth-info/', api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'), + path('system-users//applications//auth-info/', api.SystemUserAppAuthInfoApi.as_view(), name='system-user-app-auth-info'), + path('system-users//temp-auth/', api.SystemUserTempAuthInfoApi.as_view(), name='system-user-asset-temp-info'), path('system-users//tasks/', api.SystemUserTaskApi.as_view(), name='system-user-task-create'), path('system-users//cmd-filter-rules/', api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'), diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index ceebf3bde..76d6e934d 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -10,6 +10,7 @@ from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied +from rest_framework import serializers from common.utils import get_logger, random_string from common.drf.api import SerializerMixin2 @@ -49,7 +50,7 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView raise PermissionDenied(error) return True - def create_token(self, user, asset, application, system_user): + def create_token(self, user, asset, application, system_user, ttl=5*60): if not settings.CONNECTION_TOKEN_ENABLED: raise PermissionDenied('Connection token disabled') if not user: @@ -79,7 +80,7 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView }) key = self.CACHE_KEY_PREFIX.format(token) - cache.set(key, value, timeout=30*60) + cache.set(key, value, timeout=ttl) return token def create(self, request, *args, **kwargs): @@ -93,14 +94,14 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView token = self.create_token(user, asset, application, system_user) return Response({"token": token}, status=201) - @action(methods=['POST', 'GET'], detail=False, url_path='rdp/file') + @action(methods=['POST', 'GET'], detail=False, url_path='rdp/file', permission_classes=[IsValidUser]) def get_rdp_file(self, request, *args, **kwargs): options = { 'full address:s': '', 'username:s': '', 'screen mode id:i': '0', - 'desktopwidth:i': '1280', - 'desktopheight:i': '800', + # 'desktopwidth:i': '1280', + # 'desktopheight:i': '800', 'use multimon:i': '1', 'session bpp:i': '32', 'audiomode:i': '0', @@ -120,6 +121,8 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView 'autoreconnection enabled:i': '1', 'bookmarktype:i': '3', 'use redirection server name:i': '0', + 'smart sizing:i': '0', + # 'domain:s': '' # 'alternate shell:s:': '||MySQLWorkbench', # 'remoteapplicationname:s': 'Firefox', # 'remoteapplicationcmdline:s': '', @@ -134,17 +137,23 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView asset = serializer.validated_data.get('asset') application = serializer.validated_data.get('application') system_user = serializer.validated_data['system_user'] - user = serializer.validated_data.get('user') height = serializer.validated_data.get('height') width = serializer.validated_data.get('width') + user = request.user token = self.create_token(user, asset, application, system_user) - # Todo: 上线后地址是 JumpServerAddr:3389 - address = self.request.query_params.get('address') or '1.1.1.1' + address = settings.TERMINAL_RDP_ADDR + if not address or address == 'localhost:3389': + address = request.get_host().split(':')[0] + ':3389' options['full address:s'] = address options['username:s'] = '{}|{}'.format(user.username, token) - options['desktopwidth:i'] = width - options['desktopheight:i'] = height + if system_user.ad_domain: + options['domain:s'] = system_user.ad_domain + if width and height: + options['desktopwidth:i'] = width + options['desktopheight:i'] = height + else: + options['smart sizing:i'] = '1' data = '' for k, v in options.items(): data += f'{k}:{v}\n' @@ -155,10 +164,8 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView return response @staticmethod - def _get_application_secret_detail(value): - from applications.models import Application + def _get_application_secret_detail(application): from perms.models import Action - application = get_object_or_404(Application, id=value.get('application')) gateway = None if not application.category_remote_app: @@ -184,15 +191,15 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView } @staticmethod - def _get_asset_secret_detail(value, user, system_user): - from assets.models import Asset + def _get_asset_secret_detail(asset, user, system_user): from perms.utils.asset import get_asset_system_user_ids_with_actions_by_user - asset = get_object_or_404(Asset, id=value.get('asset')) systemuserid_actions_mapper = get_asset_system_user_ids_with_actions_by_user(user, asset) actions = systemuserid_actions_mapper.get(system_user.id, []) + gateway = None if asset and asset.domain and asset.domain.has_gateway(): gateway = asset.domain.random_gateway() + return { 'asset': asset, 'application': None, @@ -201,26 +208,47 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView 'actions': actions, } - @action(methods=['POST'], detail=False, permission_classes=[IsSuperUserOrAppUser], url_path='secret-info/detail') - def get_secret_detail(self, request, *args, **kwargs): + def valid_token(self, token): from users.models import User - from assets.models import SystemUser + from assets.models import SystemUser, Asset + from applications.models import Application - token = request.data.get('token', '') key = self.CACHE_KEY_PREFIX.format(token) value = cache.get(key, None) if not value: - return Response(status=404) - user = get_object_or_404(User, id=value.get('user')) - system_user = get_object_or_404(SystemUser, id=value.get('system_user')) - data = dict(user=user, system_user=system_user) + raise serializers.ValidationError('Token not found') + user = get_object_or_404(User, id=value.get('user')) + if not user.is_valid: + raise serializers.ValidationError("User not valid, disabled or expired") + + system_user = get_object_or_404(SystemUser, id=value.get('system_user')) + + asset = None + app = None if value.get('type') == 'asset': - asset_detail = self._get_asset_secret_detail(value, user=user, system_user=system_user) + asset = get_object_or_404(Asset, id=value.get('asset')) + else: + app = get_object_or_404(Application, id=value.get('application')) + + if asset and not asset.is_active: + raise serializers.ValidationError("Asset disabled") + return value, user, system_user, asset, app + + @action(methods=['POST'], detail=False, permission_classes=[IsSuperUserOrAppUser], url_path='secret-info/detail') + def get_secret_detail(self, request, *args, **kwargs): + token = request.data.get('token', '') + value, user, system_user, asset, app = self.valid_token(token) + + data = dict(user=user, system_user=system_user) + if asset: + asset_detail = self._get_asset_secret_detail(asset, user=user, system_user=system_user) + system_user.load_asset_more_auth(asset.id, user.username, user.id) data['type'] = 'asset' data.update(asset_detail) else: - app_detail = self._get_application_secret_detail(value) + app_detail = self._get_application_secret_detail(app) + system_user.load_app_more_auth(app.id, user.id) data['type'] = 'application' data.update(app_detail) diff --git a/apps/authentication/serializers.py b/apps/authentication/serializers.py index 72b54e3ee..11381c4cb 100644 --- a/apps/authentication/serializers.py +++ b/apps/authentication/serializers.py @@ -199,5 +199,5 @@ class ConnectionTokenSecretSerializer(serializers.Serializer): class RDPFileSerializer(ConnectionTokenSerializer): - width = serializers.IntegerField(default=1280) - height = serializers.IntegerField(default=800) + width = serializers.IntegerField(allow_null=True, max_value=3112, min_value=100, required=False) + height = serializers.IntegerField(allow_null=True, max_value=4096, min_value=100, required=False) diff --git a/apps/common/drf/metadata.py b/apps/common/drf/metadata.py index cc2903d2f..afd7389bb 100644 --- a/apps/common/drf/metadata.py +++ b/apps/common/drf/metadata.py @@ -18,7 +18,7 @@ from rest_framework.request import clone_request class SimpleMetadataWithFilters(SimpleMetadata): """Override SimpleMetadata, adding info about filters""" - methods = {"PUT", "POST", "GET"} + methods = {"PUT", "POST", "GET", "PATCH"} attrs = [ 'read_only', 'label', 'help_text', 'min_length', 'max_length', @@ -32,6 +32,9 @@ class SimpleMetadataWithFilters(SimpleMetadata): """ actions = {} for method in self.methods & set(view.allowed_methods): + if hasattr(view, 'action_map'): + view.action = view.action_map.get(method.lower(), view.action) + view.request = clone_request(request, method) try: # Test global permissions diff --git a/apps/common/drf/parsers/base.py b/apps/common/drf/parsers/base.py index acffcfef8..32f93a1bf 100644 --- a/apps/common/drf/parsers/base.py +++ b/apps/common/drf/parsers/base.py @@ -94,7 +94,7 @@ class BaseFileParser(BaseParser): new_row_data = {} serializer_fields = self.serializer_fields for k, v in row_data.items(): - if isinstance(v, list) or isinstance(v, dict) or isinstance(v, str) and k.strip() and v.strip(): + if type(v) in [list, dict, int] or (isinstance(v, str) and k.strip() and v.strip()): # 解决类似disk_info为字符串的'{}'的问题 if not isinstance(v, str) and isinstance(serializer_fields[k], serializers.CharField): v = str(v) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 68be2776b..81188eb8a 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -259,6 +259,7 @@ class Config(dict): 'SECURITY_INSECURE_COMMAND': False, 'SECURITY_INSECURE_COMMAND_LEVEL': 5, 'SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER': '', + 'SECURITY_LUNA_REMEMBER_AUTH': True, 'HTTP_BIND_HOST': '0.0.0.0', 'HTTP_LISTEN_PORT': 8080, @@ -300,7 +301,9 @@ class Config(dict): 'SESSION_SAVE_EVERY_REQUEST': True, 'SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE': False, 'FORGOT_PASSWORD_URL': '', - 'HEALTH_CHECK_TOKEN': '' + 'HEALTH_CHECK_TOKEN': '', + + 'TERMINAL_RDP_ADDR': '' } def compatible_auth_openid_of_key(self): diff --git a/apps/jumpserver/rewriting/__init__.py b/apps/jumpserver/rewriting/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/jumpserver/rewriting/session.py b/apps/jumpserver/rewriting/session.py new file mode 100644 index 000000000..d0e698070 --- /dev/null +++ b/apps/jumpserver/rewriting/session.py @@ -0,0 +1,18 @@ +from redis_sessions.session import force_unicode, SessionStore as RedisSessionStore +from redis import exceptions + + +class SessionStore(RedisSessionStore): + + def load(self): + try: + session_data = self.server.get( + self.get_real_stored_key(self._get_or_create_session_key()) + ) + return self.decode(force_unicode(session_data)) + except exceptions.ConnectionError as e: + # 解决redis服务异常(如: 主从切换时),用户session立即过期的问题 + raise + except: + self._session_key = None + return {} diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index 4a2e59062..268bafa44 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -48,6 +48,7 @@ INSTALLED_APPS = [ 'applications.apps.ApplicationsConfig', 'tickets.apps.TicketsConfig', 'acls.apps.AclsConfig', + 'notifications', 'common.apps.CommonConfig', 'jms_oidc_rp', 'rest_framework', @@ -125,7 +126,7 @@ SESSION_EXPIRE_AT_BROWSER_CLOSE = True # 自定义的配置,SESSION_EXPIRE_AT_BROWSER_CLOSE 始终为 True, 下面这个来控制是否强制关闭后过期 cookie SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE = CONFIG.SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE SESSION_SAVE_EVERY_REQUEST = CONFIG.SESSION_SAVE_EVERY_REQUEST -SESSION_ENGINE = 'redis_sessions.session' +SESSION_ENGINE = 'jumpserver.rewriting.session' SESSION_REDIS = { 'host': CONFIG.REDIS_HOST, 'port': CONFIG.REDIS_PORT, diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index ed37ea49c..c60c53788 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -125,3 +125,6 @@ FORGOT_PASSWORD_URL = CONFIG.FORGOT_PASSWORD_URL # 自定义默认组织名 GLOBAL_ORG_DISPLAY_NAME = CONFIG.GLOBAL_ORG_DISPLAY_NAME HEALTH_CHECK_TOKEN = CONFIG.HEALTH_CHECK_TOKEN + +TERMINAL_RDP_ADDR = CONFIG.TERMINAL_RDP_ADDR +SECURITY_LUNA_REMEMBER_AUTH = CONFIG.SECURITY_LUNA_REMEMBER_AUTH diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index 687b7f2ae..c2ffea6ec 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -23,6 +23,7 @@ api_v1 = [ path('applications/', include('applications.urls.api_urls', namespace='api-applications')), path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')), path('acls/', include('acls.urls.api_urls', namespace='api-acls')), + path('notifications/', include('notifications.urls.notifications', namespace='api-notifications')), path('prometheus/metrics/', api.PrometheusMetricsApi.as_view()), ] diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index b3bde6600..a3df15a78 100644 Binary files a/apps/locale/zh/LC_MESSAGES/django.mo and b/apps/locale/zh/LC_MESSAGES/django.mo differ diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index a64dde7cd..c6787d382 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-05-21 11:08+0800\n" +"POT-Creation-Date: 2021-06-04 11:29+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -19,7 +19,7 @@ msgstr "" #: acls/models/base.py:25 acls/serializers/login_asset_acl.py:47 #: applications/models/application.py:11 assets/models/asset.py:142 -#: assets/models/base.py:250 assets/models/cluster.py:18 +#: assets/models/base.py:249 assets/models/cluster.py:18 #: assets/models/cmd_filter.py:21 assets/models/domain.py:21 #: assets/models/group.py:20 assets/models/label.py:18 ops/mixin.py:24 #: orgs/models.py:23 perms/models/base.py:49 settings/models.py:29 @@ -35,12 +35,12 @@ msgid "Name" msgstr "名称" #: acls/models/base.py:27 assets/models/cmd_filter.py:54 -#: assets/models/user.py:122 +#: assets/models/user.py:123 msgid "Priority" msgstr "优先级" #: acls/models/base.py:28 assets/models/cmd_filter.py:54 -#: assets/models/user.py:122 +#: assets/models/user.py:123 msgid "1-100, the lower the value will be match first" msgstr "优先级可选范围为 1-100 (数值越小越优先)" @@ -54,7 +54,7 @@ msgstr "激活中" # msgstr "创建日期" #: acls/models/base.py:32 applications/models/application.py:24 #: assets/models/asset.py:147 assets/models/asset.py:223 -#: assets/models/base.py:255 assets/models/cluster.py:29 +#: assets/models/base.py:254 assets/models/cluster.py:29 #: assets/models/cmd_filter.py:23 assets/models/cmd_filter.py:64 #: assets/models/domain.py:22 assets/models/domain.py:56 #: assets/models/group.py:23 assets/models/label.py:23 ops/models/adhoc.py:37 @@ -99,7 +99,7 @@ msgstr "动作" #: terminal/backends/command/models.py:18 #: terminal/backends/command/serializers.py:12 terminal/models/session.py:38 #: tickets/models/comment.py:17 users/models/user.py:176 -#: users/models/user.py:738 users/models/user.py:764 +#: users/models/user.py:746 users/models/user.py:772 #: users/serializers/group.py:20 #: users/templates/users/user_asset_permission.html:38 #: users/templates/users/user_asset_permission.html:64 @@ -121,8 +121,8 @@ msgstr "系统用户" #: applications/serializers/attrs/application_category/remote_app.py:33 #: assets/models/asset.py:355 assets/models/authbook.py:26 #: assets/models/gathered_user.py:14 assets/serializers/admin_user.py:34 -#: assets/serializers/asset_user.py:48 assets/serializers/asset_user.py:89 -#: assets/serializers/system_user.py:201 audits/models.py:38 +#: assets/serializers/asset_user.py:48 assets/serializers/asset_user.py:90 +#: assets/serializers/system_user.py:202 audits/models.py:38 #: perms/models/asset_permission.py:99 templates/index.html:82 #: terminal/backends/command/models.py:19 #: terminal/backends/command/serializers.py:13 terminal/models/session.py:40 @@ -158,7 +158,7 @@ msgstr "" #: acls/serializers/login_acl.py:30 acls/serializers/login_asset_acl.py:31 #: applications/serializers/attrs/application_type/mysql_workbench.py:18 #: assets/models/asset.py:183 assets/models/domain.py:52 -#: assets/serializers/asset_user.py:47 settings/serializers/settings.py:112 +#: assets/serializers/asset_user.py:47 settings/serializers/settings.py:117 #: users/templates/users/_granted_assets.html:26 #: users/templates/users/user_asset_permission.html:156 msgid "IP" @@ -178,7 +178,7 @@ msgstr "格式为逗号分隔的字符串, * 表示匹配所有. " #: applications/serializers/attrs/application_type/custom.py:21 #: applications/serializers/attrs/application_type/mysql_workbench.py:30 #: applications/serializers/attrs/application_type/vmware_client.py:26 -#: assets/models/base.py:251 assets/models/gathered_user.py:15 +#: assets/models/base.py:250 assets/models/gathered_user.py:15 #: audits/models.py:100 authentication/forms.py:15 authentication/forms.py:17 #: ops/models/adhoc.py:148 users/forms/profile.py:31 users/models/user.py:548 #: users/templates/users/_select_user_modal.html:14 @@ -199,7 +199,7 @@ msgstr "" #: acls/serializers/login_asset_acl.py:35 assets/models/asset.py:184 #: assets/serializers/asset_user.py:46 assets/serializers/gathered_user.py:23 -#: settings/serializers/settings.py:111 +#: settings/serializers/settings.py:116 #: users/templates/users/_granted_assets.html:25 #: users/templates/users/user_asset_permission.html:157 msgid "Hostname" @@ -212,7 +212,7 @@ msgid "" msgstr "格式为逗号分隔的字符串, * 表示匹配所有. 可选的协议有: {}" #: acls/serializers/login_asset_acl.py:55 assets/models/asset.py:187 -#: assets/models/domain.py:54 assets/models/user.py:123 +#: assets/models/domain.py:54 assets/models/user.py:124 #: terminal/serializers/session.py:32 terminal/serializers/storage.py:69 msgid "Protocol" msgstr "协议" @@ -233,6 +233,7 @@ msgstr "所有复核人都不属于组织 `{}`" #: applications/const.py:9 #: applications/serializers/attrs/application_category/db.py:14 #: applications/serializers/attrs/application_type/mysql_workbench.py:26 +#: assets/backends/db.py:278 msgid "Database" msgstr "数据库" @@ -315,10 +316,10 @@ msgstr "目标URL" #: applications/serializers/attrs/application_type/custom.py:25 #: applications/serializers/attrs/application_type/mysql_workbench.py:34 #: applications/serializers/attrs/application_type/vmware_client.py:30 -#: assets/models/base.py:252 assets/serializers/asset_user.py:76 +#: assets/models/base.py:251 assets/serializers/asset_user.py:77 #: audits/signals_handler.py:58 authentication/forms.py:22 #: authentication/templates/authentication/login.html:164 -#: settings/serializers/settings.py:93 users/forms/profile.py:21 +#: settings/serializers/settings.py:98 users/forms/profile.py:21 #: users/templates/users/user_otp_check_password.html:13 #: users/templates/users/user_password_update.html:43 #: users/templates/users/user_password_verify.html:18 @@ -357,11 +358,35 @@ msgstr "不能删除根节点 ({})" msgid "Deletion failed and the node contains assets" msgstr "删除失败,节点包含资产" -#: assets/backends/db.py:244 +#: assets/backends/db.py:109 assets/models/user.py:304 audits/models.py:39 +#: perms/models/application_permission.py:31 +#: perms/models/asset_permission.py:101 templates/_nav.html:45 +#: terminal/backends/command/models.py:20 +#: terminal/backends/command/serializers.py:14 terminal/models/session.py:42 +#: users/templates/users/_granted_assets.html:27 +#: 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 +msgid "System user" +msgstr "系统用户" + +#: assets/backends/db.py:180 +msgid "System user(Dynamic)" +msgstr "系统用户(动态)" + +#: assets/backends/db.py:232 assets/models/asset.py:196 +#: assets/models/cluster.py:19 assets/models/user.py:67 templates/_nav.html:44 +#: xpack/plugins/cloud/models.py:92 xpack/plugins/cloud/serializers.py:160 +msgid "Admin user" +msgstr "管理用户" + +#: assets/backends/db.py:253 msgid "Could not remove asset admin user" msgstr "不能移除资产的管理用户账号" -#: assets/backends/db.py:305 +#: assets/backends/db.py:317 msgid "Latest version could not be delete" msgstr "最新版本的不能被删除" @@ -392,7 +417,7 @@ msgstr "系统平台" msgid "Protocols" msgstr "协议组" -#: assets/models/asset.py:192 assets/models/user.py:118 +#: assets/models/asset.py:192 assets/models/user.py:119 #: perms/models/asset_permission.py:100 #: xpack/plugins/change_auth_plan/models.py:56 #: xpack/plugins/gathered_user/models.py:24 @@ -405,12 +430,6 @@ msgstr "节点" msgid "Is active" msgstr "激活" -#: assets/models/asset.py:196 assets/models/cluster.py:19 -#: assets/models/user.py:66 templates/_nav.html:44 -#: xpack/plugins/cloud/models.py:92 xpack/plugins/cloud/serializers.py:160 -msgid "Admin user" -msgstr "管理用户" - #: assets/models/asset.py:199 msgid "Public IP" msgstr "公网IP" @@ -479,7 +498,7 @@ msgstr "主机名原始" msgid "Labels" msgstr "标签管理" -#: assets/models/asset.py:221 assets/models/base.py:258 +#: assets/models/asset.py:221 assets/models/base.py:257 #: assets/models/cluster.py:28 assets/models/cmd_filter.py:26 #: assets/models/cmd_filter.py:67 assets/models/group.py:21 #: common/db/models.py:70 common/mixins/models.py:49 orgs/models.py:24 @@ -491,13 +510,13 @@ msgstr "创建者" # msgid "Created by" # msgstr "创建者" -#: assets/models/asset.py:222 assets/models/base.py:256 +#: assets/models/asset.py:222 assets/models/base.py:255 #: assets/models/cluster.py:26 assets/models/domain.py:24 #: assets/models/gathered_user.py:19 assets/models/group.py:22 #: assets/models/label.py:25 common/db/models.py:72 common/mixins/models.py:50 #: ops/models/adhoc.py:38 ops/models/command.py:29 orgs/models.py:25 #: orgs/models.py:420 perms/models/base.py:56 users/models/group.py:18 -#: users/models/user.py:765 xpack/plugins/cloud/models.py:107 +#: users/models/user.py:773 xpack/plugins/cloud/models.py:107 msgid "Date created" msgstr "创建日期" @@ -517,19 +536,19 @@ msgstr "版本" msgid "AuthBook" msgstr "" -#: assets/models/base.py:253 xpack/plugins/change_auth_plan/models.py:72 +#: assets/models/base.py:252 xpack/plugins/change_auth_plan/models.py:72 #: xpack/plugins/change_auth_plan/models.py:197 #: xpack/plugins/change_auth_plan/models.py:292 msgid "SSH private key" msgstr "SSH密钥" -#: assets/models/base.py:254 xpack/plugins/change_auth_plan/models.py:75 +#: assets/models/base.py:253 xpack/plugins/change_auth_plan/models.py:75 #: xpack/plugins/change_auth_plan/models.py:193 #: xpack/plugins/change_auth_plan/models.py:288 msgid "SSH public key" msgstr "SSH公钥" -#: assets/models/base.py:257 assets/models/gathered_user.py:20 +#: assets/models/base.py:256 assets/models/gathered_user.py:20 #: common/db/models.py:73 common/mixins/models.py:51 ops/models/adhoc.py:39 #: orgs/models.py:421 msgid "Date updated" @@ -569,7 +588,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:750 +#: users/models/user.py:758 msgid "System" msgstr "系统" @@ -577,7 +596,7 @@ msgstr "系统" msgid "Default Cluster" msgstr "默认Cluster" -#: assets/models/cmd_filter.py:33 assets/models/user.py:128 +#: assets/models/cmd_filter.py:33 assets/models/user.py:129 msgid "Command filter" msgstr "命令过滤器" @@ -674,7 +693,7 @@ msgstr "全称" msgid "Parent key" msgstr "ssh私钥" -#: assets/models/node.py:559 assets/serializers/system_user.py:200 +#: assets/models/node.py:559 assets/serializers/system_user.py:201 #: users/templates/users/user_asset_permission.html:41 #: users/templates/users/user_asset_permission.html:73 #: users/templates/users/user_asset_permission.html:158 @@ -682,78 +701,64 @@ msgstr "ssh私钥" msgid "Node" msgstr "节点" -#: assets/models/user.py:114 +#: assets/models/user.py:115 msgid "Automatic login" msgstr "自动登录" -#: assets/models/user.py:115 +#: assets/models/user.py:116 msgid "Manually login" msgstr "手动登录" -#: assets/models/user.py:117 +#: assets/models/user.py:118 msgid "Username same with user" msgstr "用户名与用户相同" -#: assets/models/user.py:119 assets/serializers/domain.py:30 +#: assets/models/user.py:120 assets/serializers/domain.py:30 #: templates/_nav.html:39 xpack/plugins/change_auth_plan/models.py:52 msgid "Assets" msgstr "资产" -#: assets/models/user.py:120 templates/_nav.html:17 +#: assets/models/user.py:121 templates/_nav.html:17 #: users/views/profile/password.py:43 users/views/profile/pubkey.py:37 msgid "Users" msgstr "用户管理" -#: assets/models/user.py:121 +#: assets/models/user.py:122 msgid "User groups" msgstr "用户组" -#: assets/models/user.py:124 +#: assets/models/user.py:125 msgid "Auto push" msgstr "自动推送" -#: assets/models/user.py:125 +#: assets/models/user.py:126 msgid "Sudo" msgstr "Sudo" -#: assets/models/user.py:126 +#: assets/models/user.py:127 msgid "Shell" msgstr "Shell" -#: assets/models/user.py:127 +#: assets/models/user.py:128 msgid "Login mode" msgstr "登录模式" -#: assets/models/user.py:129 +#: assets/models/user.py:130 msgid "SFTP Root" msgstr "SFTP根路径" -#: assets/models/user.py:130 authentication/models.py:95 +#: assets/models/user.py:131 authentication/models.py:95 msgid "Token" msgstr "" -#: assets/models/user.py:131 +#: assets/models/user.py:132 msgid "Home" msgstr "家目录" -#: assets/models/user.py:132 +#: assets/models/user.py:133 msgid "System groups" msgstr "用户组" -#: assets/models/user.py:228 audits/models.py:39 -#: perms/models/application_permission.py:31 -#: perms/models/asset_permission.py:101 templates/_nav.html:45 -#: terminal/backends/command/models.py:20 -#: terminal/backends/command/serializers.py:14 terminal/models/session.py:42 -#: users/templates/users/_granted_assets.html:27 -#: 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 -msgid "System user" -msgstr "系统用户" - #: assets/models/utils.py:35 #, python-format msgid "%(value)s is not an even number" @@ -813,12 +818,16 @@ msgstr "ID" msgid "Backend" msgstr "后端" -#: assets/serializers/asset_user.py:80 users/forms/profile.py:160 +#: assets/serializers/asset_user.py:50 users/models/user.py:596 +msgid "Source" +msgstr "来源" + +#: assets/serializers/asset_user.py:81 users/forms/profile.py:160 #: users/models/user.py:580 users/templates/users/user_password_update.html:48 msgid "Public key" msgstr "SSH公钥" -#: assets/serializers/asset_user.py:84 users/models/user.py:577 +#: assets/serializers/asset_user.py:85 users/models/user.py:577 msgid "Private key" msgstr "ssh私钥" @@ -838,8 +847,8 @@ msgstr "应用数量" msgid "Gateways count" msgstr "网关数量" -#: assets/serializers/label.py:13 assets/serializers/system_user.py:47 -#: assets/serializers/system_user.py:175 +#: assets/serializers/label.py:13 assets/serializers/system_user.py:48 +#: assets/serializers/system_user.py:176 #: perms/serializers/asset/permission.py:74 msgid "Assets amount" msgstr "资产数量" @@ -861,33 +870,33 @@ msgstr "不能包含: /" msgid "The same level node name cannot be the same" msgstr "同级别节点名字不能重复" -#: assets/serializers/system_user.py:46 assets/serializers/system_user.py:174 +#: assets/serializers/system_user.py:47 assets/serializers/system_user.py:175 #: perms/serializers/asset/permission.py:75 msgid "Nodes amount" msgstr "节点数量" -#: assets/serializers/system_user.py:48 assets/serializers/system_user.py:176 -#: assets/serializers/system_user.py:202 +#: assets/serializers/system_user.py:49 assets/serializers/system_user.py:177 +#: assets/serializers/system_user.py:203 msgid "Login mode display" msgstr "登录模式(显示名称)" -#: assets/serializers/system_user.py:50 assets/serializers/system_user.py:178 +#: assets/serializers/system_user.py:51 assets/serializers/system_user.py:179 msgid "Ad domain" msgstr "Ad 网域" -#: assets/serializers/system_user.py:89 +#: assets/serializers/system_user.py:90 msgid "Username same with user with protocol {} only allow 1" msgstr "用户名和用户相同的一种协议只允许存在一个" -#: assets/serializers/system_user.py:102 +#: assets/serializers/system_user.py:103 msgid "* Automatic login mode must fill in the username." msgstr "自动登录模式,必须填写用户名" -#: assets/serializers/system_user.py:116 +#: assets/serializers/system_user.py:117 msgid "Path should starts with /" msgstr "路径应该以 / 开头" -#: assets/serializers/system_user.py:127 +#: assets/serializers/system_user.py:128 msgid "Password or private key required" msgstr "密码或密钥密码需要一个" @@ -1180,7 +1189,7 @@ msgstr "主机 (显示名称)" msgid "Result" msgstr "结果" -#: audits/serializers.py:92 terminal/serializers/storage.py:178 +#: audits/serializers.py:92 terminal/serializers/storage.py:189 msgid "Hosts" msgstr "主机" @@ -1206,11 +1215,13 @@ msgstr "" #: audits/signals_handler.py:60 #: authentication/templates/authentication/login.html:210 +#: notifications/backends/__init__.py:12 msgid "WeCom" msgstr "企业微信" #: audits/signals_handler.py:61 #: authentication/templates/authentication/login.html:215 +#: notifications/backends/__init__.py:13 msgid "DingTalk" msgstr "钉钉" @@ -1815,6 +1826,15 @@ msgstr "" "div>
如果你看到了这个页面,证明你访问的不是nginx监听的端口,祝你好运" +#: notifications/backends/__init__.py:11 users/forms/profile.py:101 +#: users/models/user.py:552 +msgid "Email" +msgstr "邮件" + +#: notifications/backends/__init__.py:14 +msgid "Site message" +msgstr "" + #: ops/api/celery.py:61 ops/api/celery.py:76 msgid "Waiting task start" msgstr "等待任务开始" @@ -1823,7 +1843,7 @@ msgstr "等待任务开始" msgid "Not has host {} permission" msgstr "没有该主机 {} 权限" -#: ops/apps.py:9 +#: ops/apps.py:9 ops/notifications.py:12 msgid "Operations" msgstr "运维" @@ -1958,6 +1978,14 @@ msgstr "命令 `{}` 不允许被执行 ......." msgid "Task end" msgstr "任务结束" +#: ops/notifications.py:13 +msgid "Server performance" +msgstr "" + +#: ops/notifications.py:20 +msgid "Disk used more than 80%: {} => {}" +msgstr "磁盘使用率超过 80%: {} => {}" + #: ops/tasks.py:71 msgid "Clean task history period" msgstr "定期清除任务历史" @@ -1974,17 +2002,13 @@ msgstr "任务列表" msgid "Update task content: {}" msgstr "更新任务内容: {}" -#: ops/utils.py:74 -msgid "Disk used more than 80%: {} => {}" -msgstr "磁盘使用率超过 80%: {} => {}" +#: orgs/api.py:77 +msgid "The current organization ({}) cannot be deleted" +msgstr "当前组织 ({}) 不能被删除" -#: orgs/api.py:79 -msgid "Have {} exists, Please delete" -msgstr "{} 存在数据, 请先删除" - -#: orgs/api.py:83 -msgid "The current organization cannot be deleted" -msgstr "当前组织不能被删除" +#: orgs/api.py:85 +msgid "The organization have resource ({}) cannot be deleted" +msgstr "组织存在资源 ({}) 不能被删除" #: orgs/mixins/models.py:45 orgs/mixins/serializers.py:25 orgs/models.py:36 #: orgs/models.py:417 orgs/serializers.py:108 @@ -2025,7 +2049,7 @@ msgstr "应用程序" msgid "Application permission" msgstr "应用管理" -#: perms/models/asset_permission.py:37 settings/serializers/settings.py:116 +#: perms/models/asset_permission.py:37 settings/serializers/settings.py:121 msgid "All" msgstr "全部" @@ -2100,8 +2124,8 @@ msgid "" msgstr "应用列表中包含与授权类型不同的应用。({})" #: perms/serializers/asset/permission.py:45 -#: perms/serializers/asset/permission.py:69 users/serializers/user.py:34 -#: users/serializers/user.py:82 +#: perms/serializers/asset/permission.py:69 users/serializers/user.py:33 +#: users/serializers/user.py:81 msgid "Is expired" msgstr "是否过期" @@ -2121,7 +2145,7 @@ msgstr "资产名称" msgid "System users name" msgstr "系统用户名称" -#: perms/serializers/asset/permission.py:70 users/serializers/user.py:81 +#: perms/serializers/asset/permission.py:70 users/serializers/user.py:80 msgid "Is valid" msgstr "账户是否有效" @@ -2167,22 +2191,30 @@ msgid "Site url" msgstr "当前站点URL" #: settings/serializers/settings.py:16 -msgid "eg: http://demo.jumpserver.org:8080" -msgstr "如: http://demo.jumpserver.org:8080" +msgid "eg: http://dev.jumpserver.org:8080" +msgstr "如: http://dev.jumpserver.org:8080" #: settings/serializers/settings.py:19 +msgid "RDP address" +msgstr "RDP 地址" + +#: settings/serializers/settings.py:21 +msgid "RDP visit address, eg: dev.jumpserver.org:3389" +msgstr "RDP 访问地址, 如: dev.jumpserver.org:3389" + +#: settings/serializers/settings.py:24 msgid "User guide url" msgstr "用户向导URL" -#: settings/serializers/settings.py:20 +#: settings/serializers/settings.py:25 msgid "User first login update profile done redirect to it" msgstr "用户第一次登录,修改profile后重定向到地址, 可以是 wiki 或 其他说明文档" -#: settings/serializers/settings.py:23 +#: settings/serializers/settings.py:28 msgid "Forgot password url" msgstr "忘记密码URL" -#: settings/serializers/settings.py:24 +#: settings/serializers/settings.py:29 msgid "" "The forgot password url on login page, If you use ldap or cas external " "authentication, you can set it" @@ -2190,138 +2222,138 @@ msgstr "" "登录页面忘记密码URL, 如果使用了 LDAP, OPENID 等外部认证系统,可以自定义用户重" "置密码访问的地址" -#: settings/serializers/settings.py:28 +#: settings/serializers/settings.py:33 msgid "Global organization name" msgstr "全局组织名" -#: settings/serializers/settings.py:29 +#: settings/serializers/settings.py:34 msgid "The name of global organization to display" msgstr "全局组织的显示名称,默认为 全局组织" -#: settings/serializers/settings.py:36 +#: settings/serializers/settings.py:41 msgid "SMTP host" msgstr "SMTP 主机" -#: settings/serializers/settings.py:37 +#: settings/serializers/settings.py:42 msgid "SMTP port" msgstr "SMTP 端口" -#: settings/serializers/settings.py:38 +#: settings/serializers/settings.py:43 msgid "SMTP account" msgstr "SMTP 账号" -#: settings/serializers/settings.py:40 +#: settings/serializers/settings.py:45 msgid "SMTP password" msgstr "SMTP 密码" -#: settings/serializers/settings.py:41 +#: settings/serializers/settings.py:46 msgid "Tips: Some provider use token except password" msgstr "提示:一些邮件提供商需要输入的是授权码" -#: settings/serializers/settings.py:44 +#: settings/serializers/settings.py:49 msgid "Send user" msgstr "发件人" -#: settings/serializers/settings.py:45 +#: settings/serializers/settings.py:50 msgid "Tips: Send mail account, default SMTP account as the send account" msgstr "提示:发送邮件账号,默认使用 SMTP 账号作为发送账号" -#: settings/serializers/settings.py:48 +#: settings/serializers/settings.py:53 msgid "Test recipient" msgstr "测试收件人" -#: settings/serializers/settings.py:49 +#: settings/serializers/settings.py:54 msgid "Tips: Used only as a test mail recipient" msgstr "提示:仅用来作为测试邮件收件人" -#: settings/serializers/settings.py:52 +#: settings/serializers/settings.py:57 msgid "Use SSL" msgstr "使用 SSL" -#: settings/serializers/settings.py:53 +#: settings/serializers/settings.py:58 msgid "If SMTP port is 465, may be select" msgstr "如果SMTP端口是465,通常需要启用 SSL" -#: settings/serializers/settings.py:56 +#: settings/serializers/settings.py:61 msgid "Use TLS" msgstr "使用 TLS" -#: settings/serializers/settings.py:57 +#: settings/serializers/settings.py:62 msgid "If SMTP port is 587, may be select" msgstr "如果SMTP端口是587,通常需要启用 TLS" -#: settings/serializers/settings.py:60 +#: settings/serializers/settings.py:65 msgid "Subject prefix" msgstr "主题前缀" -#: settings/serializers/settings.py:67 +#: settings/serializers/settings.py:72 msgid "Create user email subject" msgstr "邮件主题" -#: settings/serializers/settings.py:68 +#: settings/serializers/settings.py:73 msgid "" "Tips: When creating a user, send the subject of the email (eg:Create account " "successfully)" msgstr "提示: 创建用户时,发送设置密码邮件的主题 (例如: 创建用户成功)" -#: settings/serializers/settings.py:72 +#: settings/serializers/settings.py:77 msgid "Create user honorific" msgstr "邮件的敬语" -#: settings/serializers/settings.py:73 +#: settings/serializers/settings.py:78 msgid "Tips: When creating a user, send the honorific of the email (eg:Hello)" msgstr "提示: 创建用户时,发送设置密码邮件的敬语 (例如: 您好)" -#: settings/serializers/settings.py:77 +#: settings/serializers/settings.py:82 msgid "Create user email content" msgstr "邮件的内容" -#: settings/serializers/settings.py:78 +#: settings/serializers/settings.py:83 msgid "Tips:When creating a user, send the content of the email" msgstr "提示: 创建用户时,发送设置密码邮件的内容" -#: settings/serializers/settings.py:81 +#: settings/serializers/settings.py:86 msgid "Signature" msgstr "署名" -#: settings/serializers/settings.py:82 +#: settings/serializers/settings.py:87 msgid "Tips: Email signature (eg:jumpserver)" msgstr "邮件署名 (如:jumpserver)" -#: settings/serializers/settings.py:90 +#: settings/serializers/settings.py:95 msgid "LDAP server" msgstr "LDAP 地址" -#: settings/serializers/settings.py:90 +#: settings/serializers/settings.py:95 msgid "eg: ldap://localhost:389" msgstr "" -#: settings/serializers/settings.py:92 +#: settings/serializers/settings.py:97 msgid "Bind DN" msgstr "绑定 DN" -#: settings/serializers/settings.py:95 +#: settings/serializers/settings.py:100 msgid "User OU" msgstr "用户 OU" -#: settings/serializers/settings.py:96 +#: settings/serializers/settings.py:101 msgid "Use | split multi OUs" msgstr "多个 OU 使用 | 分割" -#: settings/serializers/settings.py:99 +#: settings/serializers/settings.py:104 msgid "User search filter" msgstr "用户过滤器" -#: settings/serializers/settings.py:100 +#: settings/serializers/settings.py:105 #, python-format msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)" msgstr "可能的选项是(cn或uid或sAMAccountName=%(user)s)" -#: settings/serializers/settings.py:103 +#: settings/serializers/settings.py:108 msgid "User attr map" msgstr "用户属性映射" -#: settings/serializers/settings.py:104 +#: settings/serializers/settings.py:109 msgid "" "User attr map present how to map LDAP user attr to jumpserver, username,name," "email is jumpserver attr" @@ -2329,23 +2361,23 @@ msgstr "" "用户属性映射代表怎样将LDAP中用户属性映射到jumpserver用户上,username, name," "email 是jumpserver的用户需要属性" -#: settings/serializers/settings.py:106 +#: settings/serializers/settings.py:111 msgid "Enable LDAP auth" msgstr "启用 LDAP 认证" -#: settings/serializers/settings.py:117 +#: settings/serializers/settings.py:122 msgid "Auto" msgstr "自动" -#: settings/serializers/settings.py:123 +#: settings/serializers/settings.py:128 msgid "Password auth" msgstr "密码认证" -#: settings/serializers/settings.py:125 +#: settings/serializers/settings.py:130 msgid "Public key auth" msgstr "密钥认证" -#: settings/serializers/settings.py:126 +#: settings/serializers/settings.py:131 msgid "" "Tips: If use other auth method, like AD/LDAP, you should disable this to " "avoid being able to log in after deleting" @@ -2353,19 +2385,19 @@ msgstr "" "提示:如果你使用其它认证方式,如 AD/LDAP,你应该禁用此项,以避免第三方系统删" "除后,还可以登录" -#: settings/serializers/settings.py:129 +#: settings/serializers/settings.py:134 msgid "List sort by" msgstr "资产列表排序" -#: settings/serializers/settings.py:130 +#: settings/serializers/settings.py:135 msgid "List page size" msgstr "资产列表每页数量" -#: settings/serializers/settings.py:132 +#: settings/serializers/settings.py:137 msgid "Session keep duration" msgstr "会话日志保存时间" -#: settings/serializers/settings.py:133 +#: settings/serializers/settings.py:138 msgid "" "Units: days, Session, record, command will be delete if more than duration, " "only in database" @@ -2373,64 +2405,64 @@ msgstr "" "单位:天。 会话、录像、命令记录超过该时长将会被删除(仅影响数据库存储, oss等不" "受影响)" -#: settings/serializers/settings.py:135 +#: settings/serializers/settings.py:140 msgid "Telnet login regex" msgstr "Telnet 成功正则表达式" -#: settings/serializers/settings.py:140 +#: settings/serializers/settings.py:145 msgid "Global MFA auth" msgstr "全局启用 MFA 认证" -#: settings/serializers/settings.py:141 +#: settings/serializers/settings.py:146 msgid "All user enable MFA" msgstr "强制所有用户启用多因子认证" -#: settings/serializers/settings.py:144 +#: settings/serializers/settings.py:149 msgid "Batch command execution" msgstr "批量命令执行" -#: settings/serializers/settings.py:145 +#: settings/serializers/settings.py:150 msgid "Allow user run batch command or not using ansible" msgstr "是否允许用户使用 ansible 执行批量命令" -#: settings/serializers/settings.py:148 +#: settings/serializers/settings.py:153 msgid "Enable terminal register" msgstr "终端注册" -#: settings/serializers/settings.py:149 +#: settings/serializers/settings.py:154 msgid "" "Allow terminal register, after all terminal setup, you should disable this " "for security" msgstr "是否允许终端注册,当所有终端启动后,为了安全应该关闭" -#: settings/serializers/settings.py:153 +#: settings/serializers/settings.py:158 msgid "Limit the number of login failures" msgstr "限制登录失败次数" -#: settings/serializers/settings.py:157 +#: settings/serializers/settings.py:162 msgid "Block logon interval" msgstr "禁止登录时间间隔" -#: settings/serializers/settings.py:158 +#: settings/serializers/settings.py:163 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/serializers/settings.py:162 +#: settings/serializers/settings.py:167 msgid "Connection max idle time" msgstr "连接最大空闲时间" -#: settings/serializers/settings.py:163 +#: settings/serializers/settings.py:168 msgid "If idle time more than it, disconnect connection Unit: minute" msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)" -#: settings/serializers/settings.py:167 +#: settings/serializers/settings.py:172 msgid "User password expiration" msgstr "用户密码过期时间" -#: settings/serializers/settings.py:168 +#: settings/serializers/settings.py:173 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 " @@ -2440,53 +2472,53 @@ msgstr "" "提示:(单位:天)如果用户在此期间没有更新密码,用户密码将过期失效; 密码过期" "提醒邮件将在密码过期前5天内由系统(每天)自动发送给用户" -#: settings/serializers/settings.py:172 +#: settings/serializers/settings.py:177 msgid "Number of repeated historical passwords" msgstr "不能设置近几次密码" -#: settings/serializers/settings.py:173 +#: settings/serializers/settings.py:178 msgid "" "Tip: When the user resets the password, it cannot be the previous n " "historical passwords of the user" msgstr "提示:用户重置密码时,不能为该用户前几次使用过的密码" -#: settings/serializers/settings.py:177 +#: settings/serializers/settings.py:182 msgid "Password minimum length" msgstr "密码最小长度" -#: settings/serializers/settings.py:180 +#: settings/serializers/settings.py:185 msgid "Must contain capital" msgstr "必须包含大写字符" -#: settings/serializers/settings.py:182 +#: settings/serializers/settings.py:187 msgid "Must contain lowercase" msgstr "必须包含小写字符" -#: settings/serializers/settings.py:183 +#: settings/serializers/settings.py:188 msgid "Must contain numeric" msgstr "必须包含数字" -#: settings/serializers/settings.py:184 +#: settings/serializers/settings.py:189 msgid "Must contain special" msgstr "必须包含特殊字符" -#: settings/serializers/settings.py:185 +#: settings/serializers/settings.py:190 msgid "Insecure command alert" msgstr "危险命令告警" -#: settings/serializers/settings.py:187 +#: settings/serializers/settings.py:192 msgid "Email recipient" msgstr "邮件收件人" -#: settings/serializers/settings.py:188 +#: settings/serializers/settings.py:193 msgid "Multiple user using , split" msgstr "多个用户,使用 , 分割" -#: settings/serializers/settings.py:196 +#: settings/serializers/settings.py:201 msgid "Enable WeCom Auth" msgstr "启用企业微信认证" -#: settings/serializers/settings.py:203 +#: settings/serializers/settings.py:208 msgid "Enable DingTalk Auth" msgstr "启用钉钉认证" @@ -2796,7 +2828,7 @@ msgstr "Web终端" msgid "File manager" msgstr "文件管理" -#: templates/_nav.html:110 terminal/apps.py:9 +#: templates/_nav.html:110 terminal/apps.py:9 terminal/notifications.py:15 #: terminal/serializers/session.py:40 msgid "Terminal" msgstr "终端" @@ -3134,20 +3166,20 @@ msgstr "风险等级(显示名称)" msgid "Timestamp" msgstr "时间戳" -#: terminal/const.py:31 +#: terminal/const.py:32 msgid "Critical" msgstr "严重" -#: terminal/const.py:32 +#: terminal/const.py:33 msgid "High" msgstr "较高" -#: terminal/const.py:33 users/templates/users/reset_password.html:50 +#: terminal/const.py:34 users/templates/users/reset_password.html:50 #: users/templates/users/user_password_update.html:104 msgid "Normal" msgstr "正常" -#: terminal/const.py:34 +#: terminal/const.py:35 msgid "Offline" msgstr "离线" @@ -3227,6 +3259,89 @@ msgstr "命令存储" msgid "Replay storage" msgstr "录像存储" +#: terminal/notifications.py:35 +msgid "Terminal command alert" +msgstr "终端命令告警" + +#: terminal/notifications.py:44 +#, python-format +msgid "" +"\n" +" Command: %(command)s\n" +"
\n" +" Asset: %(host_name)s (%(host_ip)s)\n" +"
\n" +" User: %(user)s\n" +"
\n" +" Level: %(risk_level)s\n" +"
\n" +" Session: session " +"detail\n" +"
\n" +" " +msgstr "" +"\n" +" 命令: %(command)s\n" +"
\n" +" 资产: %(host_name)s (%(host_ip)s)\n" +"
\n" +" 用户: %(user)s\n" +"
\n" +" 等级: %(risk_level)s\n" +"
\n" +" 会话: 会话详情\n" +"
\n" +" " + +#: terminal/notifications.py:79 +#, python-format +msgid "" +"Insecure Command Alert: [%(name)s->%(login_from)s@%(remote_addr)s] $" +"%(command)s" +msgstr "危险命令告警: [%(name)s->%(login_from)s@%(remote_addr)s] $%(command)s" + +#: terminal/notifications.py:97 +msgid "Batch command alert" +msgstr "批量命令告警" + +#: terminal/notifications.py:108 +#, python-format +msgid "" +"\n" +"
\n" +" Assets: %(assets)s\n" +"
\n" +" User: %(user)s\n" +"
\n" +" Level: %(risk_level)s\n" +"
\n" +"\n" +" ----------------- Commands ---------------- " +"
\n" +" %(command)s
\n" +" ----------------- Commands ---------------- " +"
\n" +" " +msgstr "" +"\n" +"
\n" +" 资产: %(assets)s\n" +"
\n" +" 用户: %(user)s\n" +"
\n" +" 等级: %(risk_level)s\n" +"
\n" +"\n" +" ----------------- 命令 ----------------
\n" +" %(command)s
\n" +" ----------------- 命令 ----------------
\n" +" " + +#: terminal/notifications.py:134 +#, python-format +msgid "Insecure Web Command Execution Alert: [%(name)s]" +msgstr "Web页面-> 命令执行 告警: [%(name)s]" + #: terminal/serializers/session.py:33 msgid "User ID" msgstr "用户 ID" @@ -3268,7 +3383,7 @@ msgid "Secret key" msgstr "" #: terminal/serializers/storage.py:39 terminal/serializers/storage.py:51 -#: terminal/serializers/storage.py:81 +#: terminal/serializers/storage.py:81 terminal/serializers/storage.py:91 msgid "Endpoint" msgstr "端点" @@ -3276,43 +3391,43 @@ msgstr "端点" msgid "Region" msgstr "地域" -#: terminal/serializers/storage.py:91 +#: terminal/serializers/storage.py:101 msgid "Container name" msgstr "容器名称" -#: terminal/serializers/storage.py:93 +#: terminal/serializers/storage.py:103 msgid "Account name" msgstr "账户名称" -#: terminal/serializers/storage.py:94 +#: terminal/serializers/storage.py:104 msgid "Account key" msgstr "账户密钥" -#: terminal/serializers/storage.py:97 +#: terminal/serializers/storage.py:107 msgid "Endpoint suffix" msgstr "端点后缀" -#: terminal/serializers/storage.py:155 +#: terminal/serializers/storage.py:166 msgid "The address format is incorrect" msgstr "地址格式不正确" -#: terminal/serializers/storage.py:162 +#: terminal/serializers/storage.py:173 msgid "Host invalid" msgstr "主机无效" -#: terminal/serializers/storage.py:165 +#: terminal/serializers/storage.py:176 msgid "Port invalid" msgstr "端口无效" -#: terminal/serializers/storage.py:181 +#: terminal/serializers/storage.py:192 msgid "Index" msgstr "索引" -#: terminal/serializers/storage.py:183 +#: terminal/serializers/storage.py:194 msgid "Doc type" msgstr "文档类型" -#: terminal/serializers/storage.py:185 +#: terminal/serializers/storage.py:196 msgid "Ignore Certificate Verification" msgstr "忽略证书认证" @@ -3320,78 +3435,6 @@ msgstr "忽略证书认证" msgid "Not found" msgstr "没有发现" -#: terminal/utils.py:78 -#, python-format -msgid "" -"Insecure Command Alert: [%(name)s->%(login_from)s@%(remote_addr)s] $" -"%(command)s" -msgstr "危险命令告警: [%(name)s->%(login_from)s@%(remote_addr)s] $%(command)s" - -#: terminal/utils.py:86 -#, python-format -msgid "" -"\n" -" Command: %(command)s\n" -"
\n" -" Asset: %(host_name)s (%(host_ip)s)\n" -"
\n" -" User: %(user)s\n" -"
\n" -" Level: %(risk_level)s\n" -"
\n" -" Session: session detail\n" -"
\n" -" " -msgstr "" -"\n" -" 命令: %(command)s\n" -"
\n" -" 资产: %(host_name)s (%(host_ip)s)\n" -"
\n" -" 用户: %(user)s\n" -"
\n" -" 等级: %(risk_level)s\n" -"
\n" -" 会话: 会话详情\n" -"
\n" -" " - -#: terminal/utils.py:113 -#, python-format -msgid "Insecure Web Command Execution Alert: [%(name)s]" -msgstr "Web页面-> 命令执行 告警: [%(name)s]" - -#: terminal/utils.py:121 -#, python-format -msgid "" -"\n" -"
\n" -" Assets: %(assets)s\n" -"
\n" -" User: %(user)s\n" -"
\n" -" Level: %(risk_level)s\n" -"
\n" -"\n" -" ----------------- Commands ----------------
\n" -" %(command)s
\n" -" ----------------- Commands ----------------
\n" -" " -msgstr "" -"\n" -"
\n" -" 资产: %(assets)s\n" -"
\n" -" 用户: %(user)s\n" -"
\n" -" 等级: %(risk_level)s\n" -"
\n" -"\n" -" ----------------- 命令 ----------------
\n" -" %(command)s
\n" -" ----------------- 命令 ----------------
\n" -" " - #: tickets/const.py:8 msgid "General" msgstr "一般" @@ -3828,10 +3871,6 @@ msgstr "确认密码" msgid "Password does not match" msgstr "密码不一致" -#: users/forms/profile.py:101 users/models/user.py:552 -msgid "Email" -msgstr "邮件" - #: users/forms/profile.py:108 msgid "Old password" msgstr "原来密码" @@ -3889,19 +3928,19 @@ msgstr "头像" msgid "Wechat" msgstr "微信" -#: users/models/user.py:596 -msgid "Source" -msgstr "用户来源" - #: users/models/user.py:600 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:746 +#: users/models/user.py:603 +msgid "Need update password" +msgstr "需要更新密码" + +#: users/models/user.py:754 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:749 +#: users/models/user.py:757 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" @@ -3909,7 +3948,7 @@ msgstr "Administrator是初始的超级管理员" msgid "The old password is incorrect" msgstr "旧密码错误" -#: users/serializers/profile.py:36 users/serializers/user.py:125 +#: users/serializers/profile.py:36 users/serializers/user.py:126 msgid "Password does not match security rules" msgstr "密码不满足安全规则" @@ -3921,76 +3960,76 @@ msgstr "新密码不能是最近 {} 次的密码" msgid "The newly set password is inconsistent" msgstr "两次密码不一致" -#: users/serializers/profile.py:119 users/serializers/user.py:80 +#: users/serializers/profile.py:119 users/serializers/user.py:79 msgid "Is first login" msgstr "首次登录" -#: users/serializers/user.py:20 +#: users/serializers/user.py:22 msgid "Reset link will be generated and sent to the user" msgstr "生成重置密码链接,通过邮件发送给用户" -#: users/serializers/user.py:21 +#: users/serializers/user.py:23 msgid "Set password" msgstr "设置密码" -#: users/serializers/user.py:28 xpack/plugins/change_auth_plan/models.py:61 +#: users/serializers/user.py:27 xpack/plugins/change_auth_plan/models.py:61 #: xpack/plugins/change_auth_plan/serializers.py:30 msgid "Password strategy" msgstr "密码策略" -#: users/serializers/user.py:30 +#: users/serializers/user.py:29 msgid "MFA enabled" msgstr "是否开启多因子认证" -#: users/serializers/user.py:31 +#: users/serializers/user.py:30 msgid "MFA force enabled" msgstr "强制启用多因子认证" -#: users/serializers/user.py:32 +#: users/serializers/user.py:31 msgid "MFA level for display" msgstr "多因子认证等级(显示名称)" -#: users/serializers/user.py:33 +#: users/serializers/user.py:32 msgid "Login blocked" msgstr "登录被阻塞" -#: users/serializers/user.py:35 +#: users/serializers/user.py:34 msgid "Can update" msgstr "是否可更新" -#: users/serializers/user.py:36 +#: users/serializers/user.py:35 msgid "Can delete" msgstr "是否可删除" -#: users/serializers/user.py:39 users/serializers/user.py:87 +#: users/serializers/user.py:38 users/serializers/user.py:86 msgid "Organization role name" msgstr "组织角色名称" -#: users/serializers/user.py:83 +#: users/serializers/user.py:82 msgid "Avatar url" msgstr "头像路径" -#: users/serializers/user.py:85 +#: users/serializers/user.py:84 msgid "Groups name" msgstr "用户组名" -#: users/serializers/user.py:86 +#: users/serializers/user.py:85 msgid "Source name" msgstr "用户来源名" -#: users/serializers/user.py:88 +#: users/serializers/user.py:87 msgid "Super role name" msgstr "超级角色名称" -#: users/serializers/user.py:89 +#: users/serializers/user.py:88 msgid "Total role name" msgstr "汇总角色名称" -#: users/serializers/user.py:113 +#: users/serializers/user.py:112 msgid "Role limit to {}" msgstr "角色只能为 {}" -#: users/serializers/user.py:210 +#: users/serializers/user.py:211 msgid "name not unique" msgstr "名称重复" @@ -5044,30 +5083,3 @@ msgstr "旗舰版" #: xpack/plugins/license/models.py:77 msgid "Community edition" msgstr "社区版" - -#~ msgid "{} is required" -#~ msgstr "{} 字段是必填项" - -#~ msgid "AppSecret is required" -#~ msgstr "AppSecret 是必须的" - -#~ msgid "Corporation ID(corpid)" -#~ msgstr "企业 ID(CorpId)" - -#~ msgid "Agent ID(agentid)" -#~ msgstr "应用 ID(AgentId)" - -#~ msgid "Secret(secret)" -#~ msgstr "秘钥(secret)" - -#~ msgid "AgentId" -#~ msgstr "应用 ID(AgentId)" - -#~ msgid "AppKey" -#~ msgstr "应用 Key(AppKey)" - -#~ msgid "AppSecret" -#~ msgstr "应用密文(AppSecret)" - -#~ msgid "No" -#~ msgstr "无" diff --git a/apps/notifications/__init__.py b/apps/notifications/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/notifications/api/__init__.py b/apps/notifications/api/__init__.py new file mode 100644 index 000000000..bde5ef849 --- /dev/null +++ b/apps/notifications/api/__init__.py @@ -0,0 +1,2 @@ +from .notifications import * +from .site_msgs import * diff --git a/apps/notifications/api/notifications.py b/apps/notifications/api/notifications.py new file mode 100644 index 000000000..7d176e7ae --- /dev/null +++ b/apps/notifications/api/notifications.py @@ -0,0 +1,72 @@ +from django.http import Http404 +from rest_framework.mixins import ListModelMixin, UpdateModelMixin +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status + +from common.drf.api import JmsGenericViewSet +from notifications.notifications import system_msgs +from notifications.models import SystemMsgSubscription +from notifications.backends import BACKEND +from notifications.serializers import ( + SystemMsgSubscriptionSerializer, SystemMsgSubscriptionByCategorySerializer +) + +__all__ = ('BackendListView', 'SystemMsgSubscriptionViewSet') + + +class BackendListView(APIView): + def get(self, request): + data = [ + { + 'name': backend, + 'name_display': backend.label + } + for backend in BACKEND + if backend.is_enable + ] + return Response(data=data) + + +class SystemMsgSubscriptionViewSet(ListModelMixin, + UpdateModelMixin, + JmsGenericViewSet): + lookup_field = 'message_type' + queryset = SystemMsgSubscription.objects.all() + serializer_classes = { + 'list': SystemMsgSubscriptionByCategorySerializer, + 'update': SystemMsgSubscriptionSerializer, + 'partial_update': SystemMsgSubscriptionSerializer + } + + def list(self, request, *args, **kwargs): + data = [] + category_children_mapper = {} + + subscriptions = self.get_queryset() + msgtype_sub_mapper = {} + for sub in subscriptions: + msgtype_sub_mapper[sub.message_type] = sub + + for msg in system_msgs: + message_type = msg['message_type'] + message_type_label = msg['message_type_label'] + category = msg['category'] + category_label = msg['category_label'] + + if category not in category_children_mapper: + children = [] + + data.append({ + 'category': category, + 'category_label': category_label, + 'children': children + }) + category_children_mapper[category] = children + + sub = msgtype_sub_mapper[message_type] + sub.message_type_label = message_type_label + category_children_mapper[category].append(sub) + + serializer = self.get_serializer(data, many=True) + return Response(data=serializer.data) diff --git a/apps/notifications/api/site_msgs.py b/apps/notifications/api/site_msgs.py new file mode 100644 index 000000000..6ee856922 --- /dev/null +++ b/apps/notifications/api/site_msgs.py @@ -0,0 +1,58 @@ +from rest_framework.response import Response +from rest_framework.mixins import ListModelMixin, RetrieveModelMixin +from rest_framework.decorators import action + +from common.http import is_true +from common.permissions import IsValidUser +from common.const.http import GET, PATCH, POST +from common.drf.api import JmsGenericViewSet +from ..serializers import ( + SiteMessageDetailSerializer, SiteMessageIdsSerializer, + SiteMessageSendSerializer, +) +from ..site_msg import SiteMessage +from ..filters import SiteMsgFilter + +__all__ = ('SiteMessageViewSet', ) + + +class SiteMessageViewSet(ListModelMixin, RetrieveModelMixin, JmsGenericViewSet): + permission_classes = (IsValidUser,) + serializer_classes = { + 'default': SiteMessageDetailSerializer, + 'mark_as_read': SiteMessageIdsSerializer, + 'send': SiteMessageSendSerializer, + } + filterset_class = SiteMsgFilter + + def get_queryset(self): + user = self.request.user + has_read = self.request.query_params.get('has_read') + + if has_read is None: + msgs = SiteMessage.get_user_all_msgs(user.id) + else: + msgs = SiteMessage.filter_user_msgs(user.id, has_read=is_true(has_read)) + return msgs + + @action(methods=[GET], detail=False, url_path='unread-total') + def unread_total(self, request, **kwargs): + user = request.user + msgs = SiteMessage.filter_user_msgs(user.id, has_read=False) + return Response(data={'total': msgs.count()}) + + @action(methods=[PATCH], detail=False, url_path='mark-as-read') + def mark_as_read(self, request, **kwargs): + user = request.user + seri = self.get_serializer(data=request.data) + seri.is_valid(raise_exception=True) + ids = seri.validated_data['ids'] + SiteMessage.mark_msgs_as_read(user.id, ids) + return Response({'detail': 'ok'}) + + @action(methods=[POST], detail=False) + def send(self, request, **kwargs): + seri = self.get_serializer(data=request.data) + seri.is_valid(raise_exception=True) + SiteMessage.send_msg(**seri.validated_data, sender=request.user) + return Response({'detail': 'ok'}) diff --git a/apps/notifications/apps.py b/apps/notifications/apps.py new file mode 100644 index 000000000..9c260e0b1 --- /dev/null +++ b/apps/notifications/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class NotificationsConfig(AppConfig): + name = 'notifications' diff --git a/apps/notifications/backends/__init__.py b/apps/notifications/backends/__init__.py new file mode 100644 index 000000000..4e2633072 --- /dev/null +++ b/apps/notifications/backends/__init__.py @@ -0,0 +1,36 @@ +from django.utils.translation import gettext_lazy as _ +from django.db import models + +from .dingtalk import DingTalk +from .email import Email +from .site_msg import SiteMessage +from .wecom import WeCom + + +class BACKEND(models.TextChoices): + EMAIL = 'email', _('Email') + WECOM = 'wecom', _('WeCom') + DINGTALK = 'dingtalk', _('DingTalk') + SITE_MSG = 'site_msg', _('Site message') + + @property + def client(self): + client = { + self.EMAIL: Email, + self.WECOM: WeCom, + self.DINGTALK: DingTalk, + self.SITE_MSG: SiteMessage + }[self] + return client + + def get_account(self, user): + return self.client.get_account(user) + + @property + def is_enable(self): + return self.client.is_enable() + + @classmethod + def filter_enable_backends(cls, backends): + enable_backends = [b for b in backends if cls(b).is_enable] + return enable_backends diff --git a/apps/notifications/backends/base.py b/apps/notifications/backends/base.py new file mode 100644 index 000000000..67a2d5b03 --- /dev/null +++ b/apps/notifications/backends/base.py @@ -0,0 +1,32 @@ +from django.conf import settings + + +class BackendBase: + # User 表中的字段 + account_field = None + + # Django setting 中的字段名 + is_enable_field_in_settings = None + + def get_accounts(self, users): + accounts = [] + unbound_users = [] + account_user_mapper = {} + + for user in users: + account = getattr(user, self.account_field, None) + if account: + account_user_mapper[account] = user + accounts.append(account) + else: + unbound_users.append(user) + return accounts, unbound_users, account_user_mapper + + @classmethod + def get_account(cls, user): + return getattr(user, cls.account_field) + + @classmethod + def is_enable(cls): + enable = getattr(settings, cls.is_enable_field_in_settings) + return bool(enable) diff --git a/apps/notifications/backends/dingtalk.py b/apps/notifications/backends/dingtalk.py new file mode 100644 index 000000000..ef5e9a9c6 --- /dev/null +++ b/apps/notifications/backends/dingtalk.py @@ -0,0 +1,20 @@ +from django.conf import settings + +from common.message.backends.dingtalk import DingTalk as Client +from .base import BackendBase + + +class DingTalk(BackendBase): + account_field = 'dingtalk_id' + is_enable_field_in_settings = 'AUTH_DINGTALK' + + def __init__(self): + self.dingtalk = Client( + appid=settings.DINGTALK_APPKEY, + appsecret=settings.DINGTALK_APPSECRET, + agentid=settings.DINGTALK_AGENTID + ) + + def send_msg(self, users, msg): + accounts, __, __ = self.get_accounts(users) + return self.dingtalk.send_text(accounts, msg) diff --git a/apps/notifications/backends/email.py b/apps/notifications/backends/email.py new file mode 100644 index 000000000..b1cdec755 --- /dev/null +++ b/apps/notifications/backends/email.py @@ -0,0 +1,14 @@ +from django.conf import settings +from django.core.mail import send_mail + +from .base import BackendBase + + +class Email(BackendBase): + account_field = 'email' + is_enable_field_in_settings = 'EMAIL_HOST_USER' + + def send_msg(self, users, subject, message): + from_email = settings.EMAIL_FROM or settings.EMAIL_HOST_USER + accounts, __, __ = self.get_accounts(users) + send_mail(subject, message, from_email, accounts) diff --git a/apps/notifications/backends/site_msg.py b/apps/notifications/backends/site_msg.py new file mode 100644 index 000000000..33032843a --- /dev/null +++ b/apps/notifications/backends/site_msg.py @@ -0,0 +1,14 @@ +from notifications.site_msg import SiteMessage as Client +from .base import BackendBase + + +class SiteMessage(BackendBase): + account_field = 'id' + + def send_msg(self, users, subject, message): + accounts, __, __ = self.get_accounts(users) + Client.send_msg(subject, message, user_ids=accounts) + + @classmethod + def is_enable(cls): + return True diff --git a/apps/notifications/backends/wecom.py b/apps/notifications/backends/wecom.py new file mode 100644 index 000000000..80b6f1a22 --- /dev/null +++ b/apps/notifications/backends/wecom.py @@ -0,0 +1,20 @@ +from django.conf import settings + +from common.message.backends.wecom import WeCom as Client +from .base import BackendBase + + +class WeCom(BackendBase): + account_field = 'wecom_id' + is_enable_field_in_settings = 'AUTH_WECOM' + + def __init__(self): + self.wecom = Client( + corpid=settings.WECOM_CORPID, + corpsecret=settings.WECOM_SECRET, + agentid=settings.WECOM_AGENTID + ) + + def send_msg(self, users, msg): + accounts, __, __ = self.get_accounts(users) + return self.wecom.send_text(accounts, msg) diff --git a/apps/notifications/filters.py b/apps/notifications/filters.py new file mode 100644 index 000000000..c28b8a3f2 --- /dev/null +++ b/apps/notifications/filters.py @@ -0,0 +1,18 @@ +import django_filters + +from common.drf.filters import BaseFilterSet +from .models import SiteMessage + + +class SiteMsgFilter(BaseFilterSet): + # 不用 Django 的关联表过滤,有个小bug,会重复关联相同表 + # SELECT DISTINCT * FROM `notifications_sitemessage` + # INNER JOIN `notifications_sitemessageusers` ON (`notifications_sitemessage`.`id` = `notifications_sitemessageusers`.`sitemessage_id`) + # INNER JOIN `notifications_sitemessageusers` T4 ON (`notifications_sitemessage`.`id` = T4.`sitemessage_id`) + # WHERE (`notifications_sitemessageusers`.`user_id` = '40c8f140dfa246d4861b80f63cf4f6e3' AND NOT T4.`has_read`) + # ORDER BY `notifications_sitemessage`.`date_created` DESC LIMIT 15; + has_read = django_filters.BooleanFilter(method='do_nothing') + + class Meta: + model = SiteMessage + fields = ('has_read',) diff --git a/apps/notifications/migrations/0001_initial.py b/apps/notifications/migrations/0001_initial.py new file mode 100644 index 000000000..ebe79f304 --- /dev/null +++ b/apps/notifications/migrations/0001_initial.py @@ -0,0 +1,92 @@ +# Generated by Django 3.1 on 2021-05-31 08:59 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('users', '0035_auto_20210526_1100'), + ] + + operations = [ + migrations.CreateModel( + name='SiteMessage', + fields=[ + ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('subject', models.CharField(max_length=1024)), + ('message', models.TextField()), + ('is_broadcast', models.BooleanField(default=False)), + ('groups', models.ManyToManyField(to='users.UserGroup')), + ('sender', models.ForeignKey(db_constraint=False, default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='send_site_message', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='UserMsgSubscription', + fields=[ + ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('message_type', models.CharField(max_length=128)), + ('receive_backends', models.JSONField(default=list)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_msg_subscriptions', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SystemMsgSubscription', + fields=[ + ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('message_type', models.CharField(max_length=128, unique=True)), + ('receive_backends', models.JSONField(default=list)), + ('groups', models.ManyToManyField(related_name='system_msg_subscriptions', to='users.UserGroup')), + ('users', models.ManyToManyField(related_name='system_msg_subscriptions', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SiteMessageUsers', + fields=[ + ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('has_read', models.BooleanField(default=False)), + ('read_at', models.DateTimeField(default=None, null=True)), + ('sitemessage', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, related_name='m2m_sitemessageusers', to='notifications.sitemessage')), + ('user', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, related_name='m2m_sitemessageusers', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='sitemessage', + name='users', + field=models.ManyToManyField(related_name='recv_site_messages', through='notifications.SiteMessageUsers', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/apps/notifications/migrations/__init__.py b/apps/notifications/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/notifications/models/__init__.py b/apps/notifications/models/__init__.py new file mode 100644 index 000000000..dede7511d --- /dev/null +++ b/apps/notifications/models/__init__.py @@ -0,0 +1,2 @@ +from .notification import * +from .site_msg import * diff --git a/apps/notifications/models/notification.py b/apps/notifications/models/notification.py new file mode 100644 index 000000000..94bd1ad7d --- /dev/null +++ b/apps/notifications/models/notification.py @@ -0,0 +1,50 @@ +from django.db import models + +from common.db.models import JMSModel + +__all__ = ('SystemMsgSubscription', 'UserMsgSubscription') + + +class UserMsgSubscription(JMSModel): + message_type = models.CharField(max_length=128) + user = models.ForeignKey('users.User', related_name='user_msg_subscriptions', on_delete=models.CASCADE) + receive_backends = models.JSONField(default=list) + + def __str__(self): + return f'{self.message_type}' + + +class SystemMsgSubscription(JMSModel): + message_type = models.CharField(max_length=128, unique=True) + users = models.ManyToManyField('users.User', related_name='system_msg_subscriptions') + groups = models.ManyToManyField('users.UserGroup', related_name='system_msg_subscriptions') + receive_backends = models.JSONField(default=list) + + message_type_label = '' + + def __str__(self): + return f'{self.message_type}' + + def __repr__(self): + return self.__str__() + + @property + def receivers(self): + from notifications.backends import BACKEND + + users = [user for user in self.users.all()] + + for group in self.groups.all(): + for user in group.users.all(): + users.append(user) + + receive_backends = self.receive_backends + receviers = [] + + for user in users: + recevier = {'name': str(user), 'id': user.id} + for backend in receive_backends: + recevier[backend] = bool(BACKEND(backend).get_account(user)) + receviers.append(recevier) + + return receviers diff --git a/apps/notifications/models/site_msg.py b/apps/notifications/models/site_msg.py new file mode 100644 index 000000000..556c00607 --- /dev/null +++ b/apps/notifications/models/site_msg.py @@ -0,0 +1,30 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from common.db.models import JMSModel + +__all__ = ('SiteMessageUsers', 'SiteMessage') + + +class SiteMessageUsers(JMSModel): + sitemessage = models.ForeignKey('notifications.SiteMessage', on_delete=models.CASCADE, db_constraint=False, related_name='m2m_sitemessageusers') + user = models.ForeignKey('users.User', on_delete=models.CASCADE, db_constraint=False, related_name='m2m_sitemessageusers') + has_read = models.BooleanField(default=False) + read_at = models.DateTimeField(default=None, null=True) + + +class SiteMessage(JMSModel): + subject = models.CharField(max_length=1024) + message = models.TextField() + users = models.ManyToManyField( + 'users.User', through=SiteMessageUsers, related_name='recv_site_messages' + ) + groups = models.ManyToManyField('users.UserGroup') + is_broadcast = models.BooleanField(default=False) + sender = models.ForeignKey( + 'users.User', db_constraint=False, on_delete=models.DO_NOTHING, null=True, default=None, + related_name='send_site_message' + ) + + has_read = False + read_at = None diff --git a/apps/notifications/notifications.py b/apps/notifications/notifications.py new file mode 100644 index 000000000..8563fd214 --- /dev/null +++ b/apps/notifications/notifications.py @@ -0,0 +1,141 @@ +from typing import Iterable +import traceback +from itertools import chain + +from django.db.utils import ProgrammingError +from celery import shared_task + +from notifications.backends import BACKEND +from .models import SystemMsgSubscription + +__all__ = ('SystemMessage', 'UserMessage') + + +system_msgs = [] +user_msgs = [] + + +class MessageType(type): + def __new__(cls, name, bases, attrs: dict): + clz = type.__new__(cls, name, bases, attrs) + + if 'message_type_label' in attrs \ + and 'category' in attrs \ + and 'category_label' in attrs: + message_type = clz.get_message_type() + + msg = { + 'message_type': message_type, + 'message_type_label': attrs['message_type_label'], + 'category': attrs['category'], + 'category_label': attrs['category_label'], + } + if issubclass(clz, SystemMessage): + system_msgs.append(msg) + try: + if not SystemMsgSubscription.objects.filter(message_type=message_type).exists(): + sub = SystemMsgSubscription.objects.create(message_type=message_type) + clz.post_insert_to_db(sub) + except ProgrammingError as e: + if e.args[0] == 1146: + # 表不存在 + pass + else: + raise + elif issubclass(clz, UserMessage): + user_msgs.append(msg) + + return clz + + +@shared_task +def publish_task(msg): + msg.publish() + + +class Message(metaclass=MessageType): + """ + 这里封装了什么? + 封装不同消息的模板,提供统一的发送消息的接口 + - publish 该方法的实现与消息订阅的表结构有关 + - send_msg + """ + + message_type_label: str + category: str + category_label: str + + @classmethod + def get_message_type(cls): + return cls.__name__ + + def publish_async(self): + return publish_task.delay(self) + + def publish(self): + raise NotImplementedError + + def send_msg(self, users: Iterable, backends: Iterable = BACKEND): + for backend in backends: + try: + backend = BACKEND(backend) + + get_msg_method = getattr(self, f'get_{backend}_msg', self.get_common_msg) + msg = get_msg_method() + client = backend.client() + + if isinstance(msg, dict): + client.send_msg(users, **msg) + else: + client.send_msg(users, msg) + except: + traceback.print_exc() + + def get_common_msg(self) -> str: + raise NotImplementedError + + def get_dingtalk_msg(self) -> str: + return self.get_common_msg() + + def get_wecom_msg(self) -> str: + return self.get_common_msg() + + def get_email_msg(self) -> dict: + msg = self.get_common_msg() + return { + 'subject': msg, + 'message': msg + } + + def get_site_msg_msg(self) -> dict: + msg = self.get_common_msg() + return { + 'subject': msg, + 'message': msg + } + + +class SystemMessage(Message): + def publish(self): + subscription = SystemMsgSubscription.objects.get( + message_type=self.get_message_type() + ) + + # 只发送当前有效后端 + receive_backends = subscription.receive_backends + receive_backends = BACKEND.filter_enable_backends(receive_backends) + + users = [ + *subscription.users.all(), + *chain(*[g.users.all() for g in subscription.groups.all()]) + ] + + self.send_msg(users, receive_backends) + + @classmethod + def post_insert_to_db(cls, subscription: SystemMsgSubscription): + pass + + +class UserMessage(Message): + pass diff --git a/apps/notifications/serializers/__init__.py b/apps/notifications/serializers/__init__.py new file mode 100644 index 000000000..bde5ef849 --- /dev/null +++ b/apps/notifications/serializers/__init__.py @@ -0,0 +1,2 @@ +from .notifications import * +from .site_msgs import * diff --git a/apps/notifications/serializers/notifications.py b/apps/notifications/serializers/notifications.py new file mode 100644 index 000000000..7415d46f7 --- /dev/null +++ b/apps/notifications/serializers/notifications.py @@ -0,0 +1,29 @@ +from rest_framework import serializers + +from common.drf.serializers import BulkModelSerializer +from notifications.models import SystemMsgSubscription + + +class SystemMsgSubscriptionSerializer(BulkModelSerializer): + receive_backends = serializers.ListField(child=serializers.CharField()) + + class Meta: + model = SystemMsgSubscription + fields = ( + 'message_type', 'message_type_label', + 'users', 'groups', 'receive_backends', 'receivers' + ) + read_only_fields = ( + 'message_type', 'message_type_label', 'receivers' + ) + extra_kwargs = { + 'users': {'allow_empty': True}, + 'groups': {'allow_empty': True}, + 'receive_backends': {'required': True} + } + + +class SystemMsgSubscriptionByCategorySerializer(serializers.Serializer): + category = serializers.CharField() + category_label = serializers.CharField() + children = SystemMsgSubscriptionSerializer(many=True) diff --git a/apps/notifications/serializers/site_msgs.py b/apps/notifications/serializers/site_msgs.py new file mode 100644 index 000000000..1f157add5 --- /dev/null +++ b/apps/notifications/serializers/site_msgs.py @@ -0,0 +1,36 @@ +from rest_framework.serializers import ModelSerializer +from rest_framework import serializers + +from ..models import SiteMessage + + +class SenderMixin(ModelSerializer): + sender = serializers.SerializerMethodField() + + def get_sender(self, site_msg): + sender = site_msg.sender + if sender: + return str(sender) + else: + return '' + + +class SiteMessageDetailSerializer(SenderMixin, ModelSerializer): + class Meta: + model = SiteMessage + fields = [ + 'id', 'subject', 'message', 'has_read', 'read_at', + 'date_created', 'date_updated', 'sender', + ] + + +class SiteMessageIdsSerializer(serializers.Serializer): + ids = serializers.ListField(child=serializers.UUIDField()) + + +class SiteMessageSendSerializer(serializers.Serializer): + subject = serializers.CharField() + message = serializers.CharField() + user_ids = serializers.ListField(child=serializers.UUIDField(), required=False) + group_ids = serializers.ListField(child=serializers.UUIDField(), required=False) + is_broadcast = serializers.BooleanField(default=False) diff --git a/apps/notifications/site_msg.py b/apps/notifications/site_msg.py new file mode 100644 index 000000000..b78d3c7f4 --- /dev/null +++ b/apps/notifications/site_msg.py @@ -0,0 +1,85 @@ +from django.db.models import F + +from common.utils.timezone import now +from users.models import User +from .models import SiteMessage as SiteMessageModel, SiteMessageUsers + + +class SiteMessage: + + @classmethod + def send_msg(cls, subject, message, user_ids=(), group_ids=(), + sender=None, is_broadcast=False): + if not any((user_ids, group_ids, is_broadcast)): + raise ValueError('No recipient is specified') + + site_msg = SiteMessageModel.objects.create( + subject=subject, message=message, + is_broadcast=is_broadcast, sender=sender, + ) + + if is_broadcast: + user_ids = User.objects.all().values_list('id', flat=True) + else: + if group_ids: + site_msg.groups.add(*group_ids) + + user_ids_from_group = User.groups.through.objects.filter( + usergroup_id__in=group_ids + ).values_list('user_id', flat=True) + + user_ids = [*user_ids, *user_ids_from_group] + + site_msg.users.add(*user_ids) + + @classmethod + def get_user_all_msgs(cls, user_id): + site_msgs = SiteMessageModel.objects.filter( + m2m_sitemessageusers__user_id=user_id + ).distinct().annotate( + has_read=F('m2m_sitemessageusers__has_read'), + read_at=F('m2m_sitemessageusers__read_at') + ).order_by('-date_created') + + return site_msgs + + @classmethod + def get_user_all_msgs_count(cls, user_id): + site_msgs_count = SiteMessageModel.objects.filter( + m2m_sitemessageusers__user_id=user_id + ).distinct().count() + return site_msgs_count + + @classmethod + def filter_user_msgs(cls, user_id, has_read=False): + site_msgs = SiteMessageModel.objects.filter( + m2m_sitemessageusers__user_id=user_id, + m2m_sitemessageusers__has_read=has_read + ).distinct().annotate( + has_read=F('m2m_sitemessageusers__has_read'), + read_at=F('m2m_sitemessageusers__read_at') + ).order_by('-date_created') + + return site_msgs + + @classmethod + def get_user_unread_msgs_count(cls, user_id): + site_msgs_count = SiteMessageModel.objects.filter( + m2m_sitemessageusers__user_id=user_id, + m2m_sitemessageusers__has_read=False + ).distinct().count() + return site_msgs_count + + @classmethod + def mark_msgs_as_read(cls, user_id, msg_ids): + sitemsg_users = SiteMessageUsers.objects.filter( + user_id=user_id, sitemessage_id__in=msg_ids, + has_read=False + ) + + for sitemsg_user in sitemsg_users: + sitemsg_user.has_read = True + sitemsg_user.read_at = now() + + SiteMessageUsers.objects.bulk_update( + sitemsg_users, fields=('has_read', 'read_at')) diff --git a/apps/notifications/tests.py b/apps/notifications/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/apps/notifications/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/notifications/urls/__init__.py b/apps/notifications/urls/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/notifications/urls/notifications.py b/apps/notifications/urls/notifications.py new file mode 100644 index 000000000..60aaee873 --- /dev/null +++ b/apps/notifications/urls/notifications.py @@ -0,0 +1,15 @@ + +from rest_framework_bulk.routes import BulkRouter +from django.urls import path + +from notifications import api + +app_name = 'notifications' + +router = BulkRouter() +router.register('system-msg-subscription', api.SystemMsgSubscriptionViewSet, 'system-msg-subscription') +router.register('site-message', api.SiteMessageViewSet, 'site-message') + +urlpatterns = [ + path('backends/', api.BackendListView.as_view(), name='backends') +] + router.urls diff --git a/apps/ops/apps.py b/apps/ops/apps.py index 8bdc04ce8..5133c6655 100644 --- a/apps/ops/apps.py +++ b/apps/ops/apps.py @@ -13,4 +13,5 @@ class OpsConfig(AppConfig): from orgs.utils import set_current_org set_current_org(Organization.root()) from .celery import signal_handler + from . import notifications super().ready() diff --git a/apps/ops/models/command.py b/apps/ops/models/command.py index 0a2012e73..e89520390 100644 --- a/apps/ops/models/command.py +++ b/apps/ops/models/command.py @@ -9,7 +9,7 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext from django.db import models -from terminal.utils import send_command_execution_alert_mail +from terminal.notifications import CommandExecutionAlert from common.utils import lazyproperty from orgs.models import Organization from orgs.mixins.models import OrgModelMixin @@ -99,12 +99,12 @@ class CommandExecution(OrgModelMixin): else: msg = _("Command `{}` is forbidden ........").format(self.command) print('\033[31m' + msg + '\033[0m') - send_command_execution_alert_mail({ + CommandExecutionAlert({ 'input': self.command, 'assets': self.hosts.all(), 'user': str(self.user), 'risk_level': 5, - }) + }).publish_async() self.result = {"error": msg} self.org_id = self.run_as.org_id self.is_finished = True diff --git a/apps/ops/notifications.py b/apps/ops/notifications.py new file mode 100644 index 000000000..61e9d5630 --- /dev/null +++ b/apps/ops/notifications.py @@ -0,0 +1,26 @@ +from django.utils.translation import gettext_lazy as _ + +from notifications.notifications import SystemMessage +from notifications.models import SystemMsgSubscription +from users.models import User + +__all__ = ('ServerPerformanceMessage',) + + +class ServerPerformanceMessage(SystemMessage): + category = 'Operations' + category_label = _('Operations') + message_type_label = _('Server performance') + + def __init__(self, path, usage): + self.path = path + self.usage = usage + + def get_common_msg(self): + msg = _("Disk used more than 80%: {} => {}").format(self.path, self.usage.percent) + return msg + + @classmethod + def post_insert_to_db(cls, subscription: SystemMsgSubscription): + admins = User.objects.filter(role=User.ROLE.ADMIN) + subscription.users.add(*admins) diff --git a/apps/ops/tasks.py b/apps/ops/tasks.py index 02cc9290e..60f639668 100644 --- a/apps/ops/tasks.py +++ b/apps/ops/tasks.py @@ -20,7 +20,7 @@ from .celery.utils import ( disable_celery_periodic_task, delete_celery_periodic_task ) from .models import Task, CommandExecution, CeleryTask -from .utils import send_server_performance_mail +from .notifications import ServerPerformanceMessage logger = get_logger(__file__) @@ -143,7 +143,7 @@ def check_server_performance_period(): if path.startswith(uncheck_path): need_check = False if need_check and usage.percent > 80: - send_server_performance_mail(path, usage, usages) + ServerPerformanceMessage(path=path, usage=usage).publish() @shared_task(queue="ansible") diff --git a/apps/ops/utils.py b/apps/ops/utils.py index 5ce4494a6..9993ea2cb 100644 --- a/apps/ops/utils.py +++ b/apps/ops/utils.py @@ -69,16 +69,6 @@ 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) - - def get_task_log_path(base_path, task_id, level=2): task_id = str(task_id) try: diff --git a/apps/orgs/api.py b/apps/orgs/api.py index ace14112b..14ad9cb20 100644 --- a/apps/orgs/api.py +++ b/apps/orgs/api.py @@ -48,7 +48,6 @@ class OrgViewSet(BulkModelViewSet): queryset = Organization.objects.all() serializer_class = OrgSerializer permission_classes = (IsSuperUserOrAppUser,) - org = None def get_serializer_class(self): mapper = { @@ -58,32 +57,36 @@ class OrgViewSet(BulkModelViewSet): return mapper.get(self.action, super().get_serializer_class()) @tmp_to_root_org() - def get_data_from_model(self, model): + def get_data_from_model(self, org, model): if model == User: data = model.objects.filter( - orgs__id=self.org.id, - m2m_org_members__role__in=[ROLE.USER, ROLE.ADMIN, ROLE.AUDITOR] + orgs__id=org.id, m2m_org_members__role__in=[ROLE.USER, ROLE.ADMIN, ROLE.AUDITOR] ) elif model == Node: - # 跟节点不能手动删除,所以排除检查 - data = model.objects.filter(org_id=self.org.id).exclude(parent_key='', key__regex=r'^[0-9]+$') + # 根节点不能手动删除,所以排除检查 + data = model.objects.filter(org_id=org.id).exclude(parent_key='', key__regex=r'^[0-9]+$') else: - data = model.objects.filter(org_id=self.org.id) + data = model.objects.filter(org_id=org.id) return data - def destroy(self, request, *args, **kwargs): - self.org = self.get_object() + def allow_bulk_destroy(self, qs, filtered): + return False + + def perform_destroy(self, instance): + if str(current_org) == str(instance): + msg = _('The current organization ({}) cannot be deleted'.format(current_org)) + raise PermissionDenied(detail=msg) + for model in org_related_models: - data = self.get_data_from_model(model) - if data: - msg = _('Have {} exists, Please delete').format(model._meta.verbose_name) - return Response(data={'error': msg}, status=status.HTTP_403_FORBIDDEN) - else: - if str(current_org) == str(self.org): - msg = _('The current organization cannot be deleted') - return Response(data={'error': msg}, status=status.HTTP_403_FORBIDDEN) - self.org.delete() - return Response({'msg': True}, status=status.HTTP_200_OK) + data = self.get_data_from_model(instance, model) + if not data: + continue + msg = _( + 'The organization have resource ({}) cannot be deleted' + ).format(model._meta.verbose_name) + raise PermissionDenied(detail=msg) + + super().perform_destroy(instance) class OrgMemberRelationBulkViewSet(JMSBulkRelationModelViewSet): diff --git a/apps/orgs/signals_handler/common.py b/apps/orgs/signals_handler/common.py index 6beecd5cb..fb8b2a2d1 100644 --- a/apps/orgs/signals_handler/common.py +++ b/apps/orgs/signals_handler/common.py @@ -167,10 +167,3 @@ def on_org_user_changed(action, instance, reverse, pk_set, **kwargs): leaved_users = set(pk_set) - set(org.members.filter(id__in=user_pk_set).values_list('id', flat=True)) _clear_users_from_org(org, leaved_users) - - -@receiver(post_save, sender=User) -def on_user_create_refresh_cache(sender, instance, created, **kwargs): - if created: - default_org = Organization.default() - default_org.members.add(instance) diff --git a/apps/settings/api/common.py b/apps/settings/api/common.py index bb3107dfd..03c885d78 100644 --- a/apps/settings/api/common.py +++ b/apps/settings/api/common.py @@ -115,6 +115,7 @@ class PublicSettingApi(generics.RetrieveAPIView): "OLD_PASSWORD_HISTORY_LIMIT_COUNT": settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT, "SECURITY_COMMAND_EXECUTION": settings.SECURITY_COMMAND_EXECUTION, "SECURITY_PASSWORD_EXPIRATION_TIME": settings.SECURITY_PASSWORD_EXPIRATION_TIME, + "SECURITY_LUNA_REMEMBER_AUTH": settings.SECURITY_LUNA_REMEMBER_AUTH, "XPACK_LICENSE_IS_VALID": has_valid_xpack_license(), "LOGIN_TITLE": self.get_login_title(), "LOGO_URLS": self.get_logo_urls(), diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py index f26679c29..f2e11c17f 100644 --- a/apps/settings/serializers/settings.py +++ b/apps/settings/serializers/settings.py @@ -13,8 +13,9 @@ __all__ = [ class BasicSettingSerializer(serializers.Serializer): SITE_URL = serializers.URLField( required=True, label=_("Site url"), - help_text=_('eg: http://demo.jumpserver.org:8080') + help_text=_('eg: http://dev.jumpserver.org:8080') ) + USER_GUIDE_URL = serializers.URLField( required=False, allow_blank=True, allow_null=True, label=_("User guide url"), help_text=_('User first login update profile done redirect to it') @@ -133,6 +134,12 @@ class TerminalSettingSerializer(serializers.Serializer): help_text=_('Units: days, Session, record, command will be delete if more than duration, only in database') ) TERMINAL_TELNET_REGEX = serializers.CharField(allow_blank=True, max_length=1024, required=False, label=_('Telnet login regex')) + TERMINAL_RDP_ADDR = serializers.CharField( + required=False, label=_("RDP address"), + max_length=1024, + allow_blank=True, + help_text=_('RDP visit address, eg: dev.jumpserver.org:3389') + ) class SecuritySettingSerializer(serializers.Serializer): diff --git a/apps/terminal/api/command.py b/apps/terminal/api/command.py index 497e40fbe..b43910e26 100644 --- a/apps/terminal/api/command.py +++ b/apps/terminal/api/command.py @@ -4,28 +4,24 @@ import time from django.conf import settings from django.utils import timezone from django.shortcuts import HttpResponse -from rest_framework import viewsets from rest_framework import generics from rest_framework.fields import DateTimeField from rest_framework.response import Response -from rest_framework.decorators import action from django.template import loader -from common.http import is_true -from terminal.models import CommandStorage, Command +from terminal.models import CommandStorage from terminal.filters import CommandFilter from orgs.utils import current_org from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor, IsAppUser -from common.const.http import GET from common.drf.api import JMSBulkModelViewSet from common.utils import get_logger -from terminal.utils import send_command_alert_mail from terminal.serializers import InsecureCommandAlertSerializer from terminal.exceptions import StorageInvalid from ..backends import ( get_command_storage, get_multi_command_storage, SessionCommandSerializer, ) +from ..notifications import CommandAlertMessage logger = get_logger(__name__) __all__ = ['CommandViewSet', 'CommandExportApi', 'InsecureCommandAlertAPI'] @@ -211,5 +207,5 @@ class InsecureCommandAlertAPI(generics.CreateAPIView): if command['risk_level'] >= settings.SECURITY_INSECURE_COMMAND_LEVEL and \ settings.SECURITY_INSECURE_COMMAND and \ settings.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER: - send_command_alert_mail(command) + CommandAlertMessage(command).publish_async() return Response() diff --git a/apps/terminal/apps.py b/apps/terminal/apps.py index f0cb05bf2..edaa38cef 100644 --- a/apps/terminal/apps.py +++ b/apps/terminal/apps.py @@ -10,4 +10,5 @@ class TerminalConfig(AppConfig): def ready(self): from . import signals_handler + from . import notifications return super().ready() diff --git a/apps/terminal/const.py b/apps/terminal/const.py index ff638325d..c2512e024 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -16,6 +16,7 @@ class ReplayStorageTypeChoices(TextChoices): swift = 'swift', 'Swift' oss = 'oss', 'OSS' azure = 'azure', 'Azure' + obs = 'obs', 'OBS' class CommandStorageTypeChoices(TextChoices): diff --git a/apps/terminal/migrations/0036_auto_20210604_1124.py b/apps/terminal/migrations/0036_auto_20210604_1124.py new file mode 100644 index 000000000..31746dfba --- /dev/null +++ b/apps/terminal/migrations/0036_auto_20210604_1124.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.6 on 2021-06-04 03:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0035_auto_20210517_1448'), + ] + + operations = [ + migrations.AlterField( + model_name='replaystorage', + name='type', + field=models.CharField(choices=[('null', 'Null'), ('server', 'Server'), ('s3', 'S3'), ('ceph', 'Ceph'), ('swift', 'Swift'), ('oss', 'OSS'), ('azure', 'Azure'), ('obs', 'OBS')], default='server', max_length=16, verbose_name='Type'), + ), + ] diff --git a/apps/terminal/models/session.py b/apps/terminal/models/session.py index 86843433e..b3202a9d9 100644 --- a/apps/terminal/models/session.py +++ b/apps/terminal/models/session.py @@ -109,8 +109,11 @@ class Session(OrgModelMixin): _PROTOCOL = self.PROTOCOL if self.is_finished: return False + if self.login_from == self.LOGIN_FROM.RT: + return False if self.protocol in [ - _PROTOCOL.SSH, _PROTOCOL.VNC, _PROTOCOL.RDP, _PROTOCOL.TELNET, _PROTOCOL.K8S + _PROTOCOL.SSH, _PROTOCOL.VNC, _PROTOCOL.RDP, + _PROTOCOL.TELNET, _PROTOCOL.K8S ]: return True else: diff --git a/apps/terminal/notifications.py b/apps/terminal/notifications.py new file mode 100644 index 000000000..fb70e3535 --- /dev/null +++ b/apps/terminal/notifications.py @@ -0,0 +1,142 @@ +from django.utils.translation import gettext_lazy as _ +from django.conf import settings + +from users.models import User +from common.utils import get_logger, reverse +from notifications.notifications import SystemMessage +from terminal.models import Session, Command +from notifications.models import SystemMsgSubscription + +logger = get_logger(__name__) + +__all__ = ('CommandAlertMessage', 'CommandExecutionAlert') + +CATEGORY = 'terminal' +CATEGORY_LABEL = _('Terminal') + + +class CommandAlertMixin: + @classmethod + def post_insert_to_db(cls, subscription: SystemMsgSubscription): + """ + 兼容操作,试图用 `settings.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER` 的邮件地址找到 + 用户,把用户设置为默认接收者 + """ + emails = settings.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER.split(',') + emails = [email.strip() for email in emails] + + users = User.objects.filter(email__in=emails) + subscription.users.add(*users) + + +class CommandAlertMessage(CommandAlertMixin, SystemMessage): + category = CATEGORY + category_label = CATEGORY_LABEL + message_type_label = _('Terminal command alert') + + def __init__(self, command): + self.command = command + + def _get_message(self): + command = self.command + session_obj = Session.objects.get(id=command['session']) + + message = _(""" + Command: %(command)s +
+ Asset: %(host_name)s (%(host_ip)s) +
+ User: %(user)s +
+ Level: %(risk_level)s +
+ Session: session detail +
+ """) % { + 'command': command['input'], + 'host_name': command['asset'], + 'host_ip': session_obj.asset_obj.ip, + 'user': command['user'], + 'risk_level': Command.get_risk_level_str(command['risk_level']), + 'session_detail_url': reverse('api-terminal:session-detail', + kwargs={'pk': command['session']}, + external=True, api_to_ui=True), + } + + return message + + def get_common_msg(self): + return self._get_message() + + def get_email_msg(self): + command = self.command + session_obj = Session.objects.get(id=command['session']) + + input = command['input'] + if isinstance(input, str): + input = input.replace('\r\n', ' ').replace('\r', ' ').replace('\n', ' ') + + subject = _("Insecure Command Alert: [%(name)s->%(login_from)s@%(remote_addr)s] $%(command)s") % { + 'name': command['user'], + 'login_from': session_obj.get_login_from_display(), + 'remote_addr': session_obj.remote_addr, + 'command': input + } + + message = self._get_message(command) + + return { + 'subject': subject, + 'message': message + } + + +class CommandExecutionAlert(CommandAlertMixin, SystemMessage): + category = CATEGORY + category_label = CATEGORY_LABEL + message_type_label = _('Batch command alert') + + def __init__(self, command): + self.command = command + + def _get_message(self): + command = self.command + input = command['input'] + input = input.replace('\n', '
') + + assets = ', '.join([str(asset) for asset in command['assets']]) + message = _(""" +
+ Assets: %(assets)s +
+ User: %(user)s +
+ Level: %(risk_level)s +
+ + ----------------- Commands ----------------
+ %(command)s
+ ----------------- Commands ----------------
+ """) % { + 'command': input, + 'assets': assets, + 'user': command['user'], + 'risk_level': Command.get_risk_level_str(command['risk_level']), + } + return message + + def get_common_msg(self): + return self._get_message() + + def get_email_msg(self): + command = self.command + + subject = _("Insecure Web Command Execution Alert: [%(name)s]") % { + 'name': command['user'], + } + message = self._get_message(command) + + return { + 'subject': subject, + 'message': message + } diff --git a/apps/terminal/serializers/storage.py b/apps/terminal/serializers/storage.py index ff794eef9..cdd6e75a3 100644 --- a/apps/terminal/serializers/storage.py +++ b/apps/terminal/serializers/storage.py @@ -82,6 +82,16 @@ class ReplayStorageTypeOSSSerializer(ReplayStorageTypeBaseSerializer): ) +class ReplayStorageTypeOBSSerializer(ReplayStorageTypeBaseSerializer): + endpoint_help_text = ''' + OBS format: obs.{REGION_NAME}.myhuaweicloud.com + Such as: obs.cn-north-4.myhuaweicloud.com + ''' + ENDPOINT = serializers.CharField( + max_length=1024, label=_('Endpoint'), help_text=_(endpoint_help_text), allow_null=True, + ) + + class ReplayStorageTypeAzureSerializer(serializers.Serializer): class EndpointSuffixChoices(TextChoices): china = 'core.chinacloudapi.cn', 'core.chinacloudapi.cn' @@ -105,7 +115,8 @@ replay_storage_type_serializer_classes_mapping = { const.ReplayStorageTypeChoices.ceph.value: ReplayStorageTypeCephSerializer, const.ReplayStorageTypeChoices.swift.value: ReplayStorageTypeSwiftSerializer, const.ReplayStorageTypeChoices.oss.value: ReplayStorageTypeOSSSerializer, - const.ReplayStorageTypeChoices.azure.value: ReplayStorageTypeAzureSerializer + const.ReplayStorageTypeChoices.azure.value: ReplayStorageTypeAzureSerializer, + const.ReplayStorageTypeChoices.obs.value: ReplayStorageTypeOBSSerializer } # ReplayStorageSerializer diff --git a/apps/terminal/utils.py b/apps/terminal/utils.py index b13383fba..68b09bcd0 100644 --- a/apps/terminal/utils.py +++ b/apps/terminal/utils.py @@ -68,78 +68,6 @@ def get_session_replay_url(session): return local_path, url -def send_command_alert_mail(command): - session_obj = Session.objects.get(id=command['session']) - - input = command['input'] - if isinstance(input, str): - input = input.replace('\r\n', ' ').replace('\r', ' ').replace('\n', ' ') - - subject = _("Insecure Command Alert: [%(name)s->%(login_from)s@%(remote_addr)s] $%(command)s") % { - 'name': command['user'], - 'login_from': session_obj.get_login_from_display(), - 'remote_addr': session_obj.remote_addr, - 'command': input - } - - recipient_list = settings.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER.split(',') - message = _(""" - Command: %(command)s -
- Asset: %(host_name)s (%(host_ip)s) -
- User: %(user)s -
- Level: %(risk_level)s -
- Session: session detail -
- """) % { - 'command': command['input'], - 'host_name': command['asset'], - 'host_ip': session_obj.asset_obj.ip, - 'user': command['user'], - 'risk_level': Command.get_risk_level_str(command['risk_level']), - 'session_detail_url': reverse('api-terminal:session-detail', - kwargs={'pk': command['session']}, - external=True, api_to_ui=True), - } - logger.debug(message) - - send_mail_async.delay(subject, message, recipient_list, html_message=message) - - -def send_command_execution_alert_mail(command): - subject = _("Insecure Web Command Execution Alert: [%(name)s]") % { - 'name': command['user'], - } - input = command['input'] - input = input.replace('\n', '
') - recipient_list = settings.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER.split(',') - - assets = ', '.join([str(asset) for asset in command['assets']]) - message = _(""" -
- Assets: %(assets)s -
- User: %(user)s -
- Level: %(risk_level)s -
- - ----------------- Commands ----------------
- %(command)s
- ----------------- Commands ----------------
- """) % { - 'command': input, - 'assets': assets, - 'user': command['user'], - 'risk_level': Command.get_risk_level_str(command['risk_level']), - } - - send_mail_async.delay(subject, message, recipient_list, html_message=message) - - class ComputeStatUtil: # system status @staticmethod diff --git a/apps/users/migrations/0035_auto_20210526_1100.py b/apps/users/migrations/0035_auto_20210526_1100.py new file mode 100644 index 000000000..4d4357a2b --- /dev/null +++ b/apps/users/migrations/0035_auto_20210526_1100.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2021-05-26 03:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0034_auto_20210506_1448'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='need_update_password', + field=models.BooleanField(default=False, verbose_name='Need update password'), + ), + ] diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 97a9e3d6d..f362e60ac 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -599,13 +599,21 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): auto_now_add=True, blank=True, null=True, verbose_name=_('Date password last updated') ) - need_update_password = models.BooleanField(default=False) + need_update_password = models.BooleanField( + default=False, verbose_name=_('Need update password') + ) wecom_id = models.CharField(null=True, default=None, unique=True, max_length=128) dingtalk_id = models.CharField(null=True, default=None, unique=True, max_length=128) def __str__(self): return '{0.name}({0.username})'.format(self) + @classmethod + def get_group_ids_by_user_id(cls, user_id): + group_ids = cls.groups.through.objects.filter(user_id=user_id).distinct().values_list('usergroup_id', flat=True) + group_ids = list(group_ids) + return group_ids + @property def is_wecom_bound(self): return bool(self.wecom_id) diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index d7591360b..46e4ca64a 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -2,6 +2,7 @@ # from django.core.cache import cache from django.utils.translation import ugettext_lazy as _ +from django.db.models import TextChoices from rest_framework import serializers from common.mixins import CommonBulkSerializerMixin @@ -17,15 +18,13 @@ __all__ = [ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): - EMAIL_SET_PASSWORD = _('Reset link will be generated and sent to the user') - CUSTOM_PASSWORD = _('Set password') - PASSWORD_STRATEGY_CHOICES = ( - (0, EMAIL_SET_PASSWORD), - (1, CUSTOM_PASSWORD) - ) + class PasswordStrategy(TextChoices): + email = 'email', _('Reset link will be generated and sent to the user') + custom = 'custom', _('Set password') + password_strategy = serializers.ChoiceField( - choices=PASSWORD_STRATEGY_CHOICES, required=False, - label=_('Password strategy'), write_only=True, default=0 + choices=PasswordStrategy.choices, default=PasswordStrategy.email, required=False, + write_only=True, label=_('Password strategy') ) mfa_enabled = serializers.BooleanField(read_only=True, label=_('MFA enabled')) mfa_force_enabled = serializers.BooleanField(read_only=True, label=_('MFA force enabled')) @@ -117,9 +116,11 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): def validate_password(self, password): from ..utils import check_password_rules password_strategy = self.initial_data.get('password_strategy') - if password_strategy == '0': + if self.instance is None and password_strategy != self.PasswordStrategy.custom: + # 创建用户,使用邮件设置密码 return - if password_strategy is None and not password: + if self.instance and not password: + # 更新用户, 未设置密码 return if not check_password_rules(password): msg = _('Password does not match security rules') diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 9dbb87017..97dac7a49 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -62,7 +62,7 @@ pytz==2018.3 PyYAML==5.1 redis==3.5.3 requests==2.22.0 -jms-storage==0.0.35 +jms-storage==0.0.37 s3transfer==0.3.3 simplejson==3.13.2 six==1.11.0