From 45741610091b017d75121b32dbc45a9e54d92890 Mon Sep 17 00:00:00 2001 From: Bai Date: Thu, 24 Nov 2022 16:44:15 +0800 Subject: [PATCH 01/65] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E6=8E=88=E6=9D=83=E8=A7=84=E5=88=99=E7=9A=84=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E5=88=97=E8=A1=A8=E4=B8=BA=E7=A9=BA=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/models/asset/common.py | 11 ----------- apps/assets/models/cmd_filter.py | 4 ++-- apps/perms/models/asset_permission.py | 2 +- apps/perms/urls/api_urls.py | 3 +-- 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/apps/assets/models/asset/common.py b/apps/assets/models/asset/common.py index c9baf8818..f611044cb 100644 --- a/apps/assets/models/asset/common.py +++ b/apps/assets/models/asset/common.py @@ -211,17 +211,6 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel): tree_node = TreeNode(**data) return tree_node - def filter_accounts(self, account_names=None): - from perms.models import AssetPermission - if account_names is None: - return self.accounts.all() - if AssetPermission.SpecialAccount.ALL in account_names: - return self.accounts.all() - # queries = Q(name__in=account_names) | Q(username__in=account_names) - queries = Q(username__in=account_names) - accounts = self.accounts.filter(queries) - return accounts - class Meta: unique_together = [('org_id', 'name')] verbose_name = _("Asset") diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py index 7023fdbc6..5b9b1ad85 100644 --- a/apps/assets/models/cmd_filter.py +++ b/apps/assets/models/cmd_filter.py @@ -183,7 +183,7 @@ class CommandFilterRule(OrgModelMixin): cls, user_id=None, user_group_id=None, account=None, asset_id=None, org_id=None ): - from perms.models.const import SpecialAccount + from assets.models import Account user_groups = [] user = get_object_or_none(User, pk=user_id) if user: @@ -202,7 +202,7 @@ class CommandFilterRule(OrgModelMixin): if account: org_id = account.org_id q |= Q(accounts__contains=account.username) | \ - Q(accounts__contains=SpecialAccount.ALL.value) + Q(accounts__contains=Account.AliasAccount.ALL) if asset: org_id = asset.org_id q |= Q(assets=asset) diff --git a/apps/perms/models/asset_permission.py b/apps/perms/models/asset_permission.py index 6f48c05d1..aa61ffe14 100644 --- a/apps/perms/models/asset_permission.py +++ b/apps/perms/models/asset_permission.py @@ -125,7 +125,7 @@ class AssetPermission(OrgModelMixin): """ asset_ids = self.get_all_assets(flat=True) q = Q(asset_id__in=asset_ids) - if Account.AliasAccount.ALL in self.accounts: + if Account.AliasAccount.ALL not in self.accounts: q &= Q(username__in=self.accounts) accounts = Account.objects.filter(q).order_by('asset__name', 'name', 'username') if not flat: diff --git a/apps/perms/urls/api_urls.py b/apps/perms/urls/api_urls.py index 9a4b3f10a..5fce2cb39 100644 --- a/apps/perms/urls/api_urls.py +++ b/apps/perms/urls/api_urls.py @@ -5,5 +5,4 @@ from .user_permission import user_permission_urlpatterns app_name = 'perms' -urlpatterns = asset_permission_urlpatterns \ - + user_permission_urlpatterns +urlpatterns = asset_permission_urlpatterns + user_permission_urlpatterns From 99f5c02d844c97e1b3ff0ee008550ee34da6a925 Mon Sep 17 00:00:00 2001 From: Bai Date: Thu, 24 Nov 2022 17:04:27 +0800 Subject: [PATCH 02/65] =?UTF-8?q?perf:=20=E8=8E=B7=E5=8F=96=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=8E=88=E6=9D=83=E7=9A=84=E8=B4=A6=E5=8F=B7=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=20has=5Fusername=20=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/models/base.py | 4 ++++ apps/perms/serializers/user_permission.py | 8 ++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index 90fb384e6..cc6ea17fe 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -62,6 +62,10 @@ class BaseAccount(JMSOrgBaseModel): def has_secret(self): return bool(self.secret) + @property + def has_username(self): + return bool(self.username) + @property def specific(self): data = {} diff --git a/apps/perms/serializers/user_permission.py b/apps/perms/serializers/user_permission.py index 1d795d650..3b60d25bb 100644 --- a/apps/perms/serializers/user_permission.py +++ b/apps/perms/serializers/user_permission.py @@ -12,7 +12,7 @@ from perms.serializers.permission import ActionChoicesField __all__ = [ 'NodeGrantedSerializer', 'AssetGrantedSerializer', - 'ActionsSerializer', 'AccountsPermedSerializer' + 'AccountsPermedSerializer' ] @@ -43,14 +43,10 @@ class NodeGrantedSerializer(serializers.ModelSerializer): read_only_fields = fields -class ActionsSerializer(serializers.Serializer): - actions = ActionChoicesField(read_only=True) - - class AccountsPermedSerializer(serializers.ModelSerializer): actions = ActionChoicesField(read_only=True) class Meta: model = Account - fields = ['id', 'name', 'username', 'secret_type', 'has_secret', 'actions'] + fields = ['id', 'name', 'has_username', 'username', 'has_secret', 'secret_type', 'actions'] read_only_fields = fields From 276f64479477c5ac37e0fa78323b247b066d4700 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Thu, 24 Nov 2022 21:21:25 +0800 Subject: [PATCH 03/65] perf: gateway (#9121) Co-authored-by: feng <1304903146@qq.com> --- apps/assets/api/domain.py | 3 ++- apps/assets/serializers/domain.py | 45 ++++++++++++++++++++++++------- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/apps/assets/api/domain.py b/apps/assets/api/domain.py index bb705322c..b98cd7273 100644 --- a/apps/assets/api/domain.py +++ b/apps/assets/api/domain.py @@ -1,5 +1,5 @@ # ~*~ coding: utf-8 ~*~ - +from django.db.models import F from django.views.generic.detail import SingleObjectMixin from django.utils.translation import ugettext as _ from rest_framework.views import APIView, Response @@ -29,6 +29,7 @@ class DomainViewSet(OrgBulkModelViewSet): class GatewayViewSet(OrgBulkModelViewSet): + perm_model = Host filterset_fields = ("domain__name", "name", "domain") search_fields = ("domain__name",) serializer_class = serializers.GatewaySerializer diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py index 64c8c032c..9e495d104 100644 --- a/apps/assets/serializers/domain.py +++ b/apps/assets/serializers/domain.py @@ -4,12 +4,13 @@ from rest_framework import serializers from rest_framework.generics import get_object_or_404 from django.utils.translation import ugettext_lazy as _ -from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from common.drf.serializers import SecretReadableMixin +from orgs.mixins.serializers import BulkOrgResourceModelSerializer, OrgResourceSerializerMixin +from common.drf.serializers import SecretReadableMixin, WritableNestedModelSerializer from common.drf.fields import ObjectRelatedField, EncryptedField -from assets.const import SecretType -from ..models import Domain, Asset, Account -from ..serializers import HostSerializer +from assets.models import Platform, Node +from assets.const import SecretType, GATEWAY_NAME +from ..serializers import AssetProtocolsSerializer +from ..models import Domain, Asset, Account, Host from .utils import validate_password_for_ansible, validate_ssh_key @@ -41,7 +42,7 @@ class DomainSerializer(BulkOrgResourceModelSerializer): return obj.gateways.count() -class GatewaySerializer(HostSerializer): +class GatewaySerializer(BulkOrgResourceModelSerializer, WritableNestedModelSerializer): password = EncryptedField( label=_('Password'), required=False, allow_blank=True, allow_null=True, max_length=1024, validators=[validate_password_for_ansible], write_only=True @@ -55,13 +56,27 @@ class GatewaySerializer(HostSerializer): max_length=512, ) username = serializers.CharField( - label=_('Username'), allow_blank=True, max_length=128, required=True, + label=_('Username'), allow_blank=True, max_length=128, required=True, write_only=True ) + username_display = serializers.SerializerMethodField(label=_('Username')) + protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols')) - class Meta(HostSerializer.Meta): - fields = HostSerializer.Meta.fields + [ - 'username', 'password', 'private_key', 'passphrase' + class Meta: + model = Host + fields_mini = ['id', 'name', 'address'] + fields_small = fields_mini + ['is_active', 'comment'] + fields = fields_small + ['domain', 'protocols'] + [ + 'username', 'password', 'private_key', 'passphrase', 'username_display' ] + extra_kwargs = { + 'name': {'label': _("Name")}, + 'address': {'label': _('Address')}, + } + + @staticmethod + def get_username_display(obj): + account = obj.accounts.order_by('-privileged').first() + return account.username if account else '' def validate_private_key(self, secret): if not secret: @@ -79,6 +94,15 @@ class GatewaySerializer(HostSerializer): validated_data.pop('passphrase', None) return username, password, private_key + @staticmethod + def generate_default_data(): + platform = Platform.objects.get(name=GATEWAY_NAME, internal=True) + # node = Node.objects.all().order_by('date_created').first() + data = { + 'platform': platform, + } + return data + @staticmethod def create_accounts(instance, username, password, private_key): account_name = f'{instance.name}-{_("Gateway")}' @@ -112,6 +136,7 @@ class GatewaySerializer(HostSerializer): def create(self, validated_data): auth_fields = self.clean_auth_fields(validated_data) + validated_data.update(self.generate_default_data()) instance = super().create(validated_data) self.create_accounts(instance, *auth_fields) return instance From 608e0c9f26b81e734a7a7b57433fc4b0dbdb82ae Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 25 Nov 2022 18:06:22 +0800 Subject: [PATCH 04/65] feat: support ed25519 key --- apps/assets/models/base.py | 24 +++---- apps/assets/serializers/utils.py | 11 +--- apps/common/utils/encode.py | 107 ++++++++++++++++++++++--------- 3 files changed, 89 insertions(+), 53 deletions(-) diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index cc6ea17fe..70c6b54e6 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -1,23 +1,22 @@ # -*- coding: utf-8 -*- # -import io import os -import sshpubkeys from hashlib import md5 -from django.db import models +import sshpubkeys from django.conf import settings -from django.utils import timezone +from django.db import models from django.db.models import QuerySet +from django.utils import timezone from django.utils.translation import ugettext_lazy as _ +from assets.const import Connectivity, SecretType +from common.db import fields from common.utils import ( ssh_key_string_to_obj, ssh_key_gen, get_logger, - random_string, ssh_pubkey_gen, lazyproperty + random_string, lazyproperty, parse_ssh_public_key_str ) -from common.db import fields from orgs.mixins.models import JMSOrgBaseModel -from assets.const import Connectivity, SecretType logger = get_logger(__file__) @@ -88,7 +87,7 @@ class BaseAccount(JMSOrgBaseModel): @lazyproperty def public_key(self): if self.secret_type == SecretType.SSH_KEY: - return ssh_pubkey_gen(private_key=self.private_key) + return parse_ssh_public_key_str(self.private_key) return None @property @@ -97,7 +96,7 @@ class BaseAccount(JMSOrgBaseModel): public_key = self.public_key elif self.private_key: try: - public_key = ssh_pubkey_gen(private_key=self.private_key) + public_key = parse_ssh_public_key_str(self.private_key) except IOError as e: return str(e) else: @@ -129,12 +128,9 @@ class BaseAccount(JMSOrgBaseModel): return key_path def get_private_key(self): - if not self.private_key_obj: + if not self.private_key: return None - string_io = io.StringIO() - self.private_key_obj.write_private_key(string_io) - private_key = string_io.getvalue() - return private_key + return self.private_key @property def public_key_obj(self): diff --git a/apps/assets/serializers/utils.py b/apps/assets/serializers/utils.py index 0734bc9f1..770710843 100644 --- a/apps/assets/serializers/utils.py +++ b/apps/assets/serializers/utils.py @@ -1,9 +1,7 @@ -from io import StringIO - from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.utils import ssh_private_key_gen, validate_ssh_private_key +from common.utils import validate_ssh_private_key, parse_ssh_private_key_str def validate_password_for_ansible(password): @@ -24,9 +22,4 @@ def validate_ssh_key(ssh_key, passphrase=None): valid = validate_ssh_private_key(ssh_key, password=passphrase) if not valid: raise serializers.ValidationError(_("private key invalid or passphrase error")) - - ssh_key = ssh_private_key_gen(ssh_key, password=passphrase) - string_io = StringIO() - ssh_key.write_private_key(string_io) - ssh_key = string_io.getvalue() - return ssh_key + return parse_ssh_private_key_str(ssh_key, passphrase) diff --git a/apps/common/utils/encode.py b/apps/common/utils/encode.py index 2bf02ac4c..db5aee795 100644 --- a/apps/common/utils/encode.py +++ b/apps/common/utils/encode.py @@ -1,24 +1,23 @@ # -*- coding: utf-8 -*- # -import re -import json -from six import string_types import base64 -import os -import time import hashlib +import json +import os +import re +import time from io import StringIO -from itertools import chain import paramiko import sshpubkeys +from cryptography.hazmat.primitives import serialization +from django.conf import settings +from django.core.serializers.json import DjangoJSONEncoder from itsdangerous import ( TimedJSONWebSignatureSerializer, JSONWebSignatureSerializer, BadSignature, SignatureExpired ) -from django.conf import settings -from django.core.serializers.json import DjangoJSONEncoder -from django.db.models.fields.files import FileField +from six import string_types from .http import http_date @@ -69,22 +68,19 @@ class Signer(metaclass=Singleton): return None +_supported_paramiko_ssh_key_types = (paramiko.RSAKey, paramiko.DSSKey, paramiko.Ed25519Key) + + def ssh_key_string_to_obj(text, password=None): key = None - try: - key = paramiko.RSAKey.from_private_key(StringIO(text), password=password) - except paramiko.SSHException: - pass - else: - return key - - try: - key = paramiko.DSSKey.from_private_key(StringIO(text), password=password) - except paramiko.SSHException: - pass - else: - return key - + for ssh_key_type in _supported_paramiko_ssh_key_types: + if not isinstance(ssh_key_type, paramiko.PKey): + continue + try: + key = ssh_key_type.from_private_key(StringIO(text), password=password) + return key + except paramiko.SSHException: + pass return key @@ -137,17 +133,68 @@ def ssh_key_gen(length=2048, type='rsa', password=None, username='jumpserver', h def validate_ssh_private_key(text, password=None): - if isinstance(text, bytes): + if isinstance(text, str): try: - text = text.decode("utf-8") + text = text.encode("utf-8") + except UnicodeDecodeError: + return False + if isinstance(password, str): + try: + password = password.encode("utf-8") except UnicodeDecodeError: return False - key = ssh_key_string_to_obj(text, password=password) - if key is None: - return False - else: - return True + key = parse_ssh_private_key_str(text, password=password) + return bool(key) + + +def parse_ssh_private_key_str(text: bytes, password=None) -> str: + private_key = _parse_ssh_private_key(text, password=password) + if private_key is None: + return "" + private_key_bytes = private_key.private_bytes(serialization.Encoding.PEM, + serialization.PrivateFormat.OpenSSH, + serialization.NoEncryption()) + return private_key_bytes.decode('utf-8') + + +def parse_ssh_public_key_str(text: bytes = "", password=None) -> str: + private_key = _parse_ssh_private_key(text, password=password) + if private_key is None: + return "" + public_key_bytes = private_key.public_key().public_bytes(serialization.Encoding.OpenSSH, + serialization.PublicFormat.OpenSSH) + return public_key_bytes.decode('utf-8') + + +def _parse_ssh_private_key(text, password=None): + """ + text: bytes + password: str + return:private key types: + ec.EllipticCurvePrivateKey, + rsa.RSAPrivateKey, + dsa.DSAPrivateKey, + ed25519.Ed25519PrivateKey, + """ + if isinstance(text, str): + try: + text = text.encode("utf-8") + except UnicodeDecodeError: + return None + if password is not None: + if isinstance(password, str): + try: + password = password.encode("utf-8") + except UnicodeDecodeError: + return None + + try: + private_key = serialization.load_ssh_private_key(text, password=password) + return private_key + except (ValueError, TypeError): + pass + return None def validate_ssh_public_key(text): From 0f35b3dd582b44bb6ba25cb684fe45da816b500d Mon Sep 17 00:00:00 2001 From: ibuler Date: Fri, 25 Nov 2022 23:09:55 +0800 Subject: [PATCH 05/65] =?UTF-8?q?pref:=20=E4=BF=AE=E6=94=B9=20connect=20to?= =?UTF-8?q?ken?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/api/mixin.py | 3 -- apps/assets/models/asset/common.py | 4 -- apps/authentication/api/connection_token.py | 17 +++--- apps/authentication/api/perm_token.py | 0 apps/authentication/api/temp_token.py | 2 +- .../migrations/0015_auto_20221125_2240.py | 49 ++++++++++++++++++ .../authentication/models/connection_token.py | 7 +-- .../serializers/connection_token.py | 10 ++-- apps/perms/api/perm_token.py | 5 -- apps/static/img/logo_text_white.png | Bin 8320 -> 5027 bytes 10 files changed, 67 insertions(+), 30 deletions(-) create mode 100644 apps/authentication/api/perm_token.py create mode 100644 apps/authentication/migrations/0015_auto_20221125_2240.py diff --git a/apps/assets/api/mixin.py b/apps/assets/api/mixin.py index 9452a76f5..f7f788e72 100644 --- a/apps/assets/api/mixin.py +++ b/apps/assets/api/mixin.py @@ -68,9 +68,6 @@ class SerializeToTreeNodeMixin: 'data': { 'id': asset.id, 'name': asset.name, - 'address': asset.address, - 'protocols': asset.protocols_as_list, - 'platform': asset.platform.id, 'org_name': asset.org_name }, } diff --git a/apps/assets/models/asset/common.py b/apps/assets/models/asset/common.py index c9baf8818..c7012bc60 100644 --- a/apps/assets/models/asset/common.py +++ b/apps/assets/models/asset/common.py @@ -160,10 +160,6 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel): return 0 return self.primary_protocol.port - @property - def protocols_as_list(self): - return [{'name': p.name, 'port': p.port} for p in self.protocols.all()] - @lazyproperty def type(self): return self.platform.type diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 1636c2cb1..59b0b7593 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -15,8 +15,8 @@ from rest_framework.response import Response from common.drf.api import JMSModelViewSet from common.http import is_true +from common.utils import random_string from orgs.mixins.api import RootOrgViewMixin -from orgs.utils import tmp_to_root_org from perms.models import ActionChoices from terminal.models import EndpointRule from ..models import ConnectionToken @@ -249,10 +249,6 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView serializer = self.get_serializer(instance=token) return Response(serializer.data, status=status.HTTP_200_OK) - def dispatch(self, request, *args, **kwargs): - with tmp_to_root_org(): - return super().dispatch(request, *args, **kwargs) - def get_queryset(self): return ConnectionToken.objects.filter(user=self.request.user) @@ -269,16 +265,17 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView data = serializer.validated_data user = self.get_user(serializer) asset = data.get('asset') - login = data.get('login') + account_name = data.get('account_name') data['org_id'] = asset.org_id data['user'] = user + data['value'] = random_string(16) util = PermAccountUtil() - permed_account = util.validate_permission(user, asset, login) + permed_account = util.validate_permission(user, asset, account_name) if not permed_account or not permed_account.actions: msg = 'user `{}` not has asset `{}` permission for login `{}`'.format( - user, asset, login + user, asset, account_name ) raise PermissionDenied(msg) @@ -286,9 +283,9 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView raise PermissionDenied('Expired') if permed_account.has_secret: - data['secret'] = '' + data['input_secret'] = '' if permed_account.username != '@INPUT': - data['username'] = '' + data['input_username'] = '' return permed_account diff --git a/apps/authentication/api/perm_token.py b/apps/authentication/api/perm_token.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/authentication/api/temp_token.py b/apps/authentication/api/temp_token.py index 6e640edd6..2fa5791e3 100644 --- a/apps/authentication/api/temp_token.py +++ b/apps/authentication/api/temp_token.py @@ -2,10 +2,10 @@ from django.utils import timezone from rest_framework.response import Response from rest_framework.decorators import action +from rbac.permissions import RBACPermission from common.drf.api import JMSModelViewSet from ..models import TempToken from ..serializers import TempTokenSerializer -from rbac.permissions import RBACPermission class TempTokenViewSet(JMSModelViewSet): diff --git a/apps/authentication/migrations/0015_auto_20221125_2240.py b/apps/authentication/migrations/0015_auto_20221125_2240.py new file mode 100644 index 000000000..7b1c073e8 --- /dev/null +++ b/apps/authentication/migrations/0015_auto_20221125_2240.py @@ -0,0 +1,49 @@ +# Generated by Django 3.2.14 on 2022-11-25 14:40 + +import common.db.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0014_auto_20221122_2152'), + ] + + operations = [ + migrations.RenameField( + model_name='connectiontoken', + old_name='login', + new_name='account_name' + ), + migrations.RenameField( + model_name='connectiontoken', + old_name='secret', + new_name='value', + ), + migrations.RenameField( + model_name='connectiontoken', + old_name='username', + new_name='input_username', + ), + migrations.AddField( + model_name='connectiontoken', + name='input_secret', + field=common.db.fields.EncryptCharField(default='', max_length=128, verbose_name='Input Secret'), + ), + migrations.AlterField( + model_name='connectiontoken', + name='account_name', + field=models.CharField(max_length=128, verbose_name='Account name'), + ), + migrations.AlterField( + model_name='connectiontoken', + name='input_username', + field=models.CharField(default='', max_length=128, verbose_name='Input Username'), + ), + migrations.AlterField( + model_name='connectiontoken', + name='value', + field=models.CharField(default='', max_length=64, verbose_name='Value'), + ), + ] diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py index 058d07581..d0a1d8478 100644 --- a/apps/authentication/models/connection_token.py +++ b/apps/authentication/models/connection_token.py @@ -19,6 +19,7 @@ def date_expired_default(): class ConnectionToken(OrgModelMixin, JMSBaseModel): + value = models.CharField(max_length=64, default='', verbose_name=_("Value")) user = models.ForeignKey( 'users.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='connection_tokens', verbose_name=_('User') @@ -27,9 +28,9 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel): 'assets.Asset', on_delete=models.SET_NULL, null=True, blank=True, related_name='connection_tokens', verbose_name=_('Asset'), ) - login = models.CharField(max_length=128, verbose_name=_("Login account")) - username = models.CharField(max_length=128, default='', verbose_name=_("Username")) - secret = EncryptCharField(max_length=64, default='', verbose_name=_("Secret")) + account_name = models.CharField(max_length=128, verbose_name=_("Account name")) # 登录账号Name + input_username = models.CharField(max_length=128, default='', verbose_name=_("Input Username")) + input_secret = EncryptCharField(max_length=64, default='', verbose_name=_("Input Secret")) protocol = models.CharField( choices=Protocol.choices, max_length=16, default=Protocol.ssh, verbose_name=_("Protocol") ) diff --git a/apps/authentication/serializers/connection_token.py b/apps/authentication/serializers/connection_token.py index 77981cd4a..db6b35963 100644 --- a/apps/authentication/serializers/connection_token.py +++ b/apps/authentication/serializers/connection_token.py @@ -15,15 +15,14 @@ __all__ = [ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): - username = serializers.CharField(max_length=128, label=_("Input username"), - allow_null=True, allow_blank=True) expire_time = serializers.IntegerField(read_only=True, label=_('Expired time')) class Meta: model = ConnectionToken - fields_mini = ['id'] + fields_mini = ['id', 'value'] fields_small = fields_mini + [ - 'protocol', 'login', 'secret', 'username', + 'protocol', 'account_name', + 'input_username', 'input_secret', 'actions', 'date_expired', 'date_created', 'date_updated', 'created_by', 'updated_by', 'org_id', 'org_name', @@ -37,6 +36,9 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): 'user_display', 'asset_display', ] fields = fields_small + fields_fk + read_only_fields + extra_kwargs = { + 'value': {'read_only': True}, + } def get_request_user(self): request = self.context.get('request') diff --git a/apps/perms/api/perm_token.py b/apps/perms/api/perm_token.py index 63cf08062..e69de29bb 100644 --- a/apps/perms/api/perm_token.py +++ b/apps/perms/api/perm_token.py @@ -1,5 +0,0 @@ -from rest_framework.viewsets import ModelViewSet - - -class PermTokenViewSet(ModelViewSet): - pass diff --git a/apps/static/img/logo_text_white.png b/apps/static/img/logo_text_white.png index 39dea6778055a6d3bae745d3c749989dad7240a5..f791baa71a2599846091805fc5c7027fe43857bf 100644 GIT binary patch literal 5027 zcmeHL_cI&-6D4{%mm@k!kf!96(>c*OoYPzM9?{F`1gE#?ov7iIBx;0cmymEeIqlTw z(V~m;`TmaY+nKj-c6NVyGjC^hW3@C@0F*401Ox;Cn5v@AEg#-Oh@9v)cjU9P-4daf zj*2|t^iRIf+k(Pf)!2)GfQs(FASB4mXCfdVBGA&%Q@U;5{6GG$!2hZOWKb&dTeA>& z>uBl{P&^L~)uOpWDkv!~A`R1JWaCj*iFgbHMb4tGwqO~ z#bxAWsd*V#BT4L zQ`Xs6E@bQbuzl;3OVw0wTJiG5ctUM!LuK?tOkG^kkDc$7AJ_ev28Y0ZX2SkXzW=E| ze6*VCgK2YXF%BHC=6&1|k!h(EUQmdHdYdhEjl3Hxjh%HWN!Li(?vI)as?M(YDwAKI z{KeL@@G}7cPynVVr{|ZylN%^KF@4kbqAUlb3ueY(vzY`rk!I2Tj}@4#<0u4IQtw7- zp){U|z>RqD8R~gzF2RP}#+y893#x6~^IyGwykyp?!`r!;rv*K21gETYWDJ*n_i;a4?n89(25)5s@mZzo>pew-CSqA~ z{b+OkR^>r1#yGWN(;9tjD}^Zm@1=|pMG21`gSV6u>o-Vsk6=;Cx{2_Pv#|+{>A9Y< zxJzTiYaWb=_Gs^1(998YGxCH6!hW3NRFS)Kpjos>;2^$@9cx5-dR+A+lC17p3C)hj zLmlc`fYBFKtYT`p>r%5o%u&&UyFZk2jN9KW=Fm|q5e&?l#~RM}ye686hrKVe|AOkSdX%L`12zNCY^8{%rTJ1d!^V zSDh>LkM`Au+gO)nH&sS9bX13IzHmV##7r-h_toXNr#|JQk}h``?26NrQYPu1JjV*E zq%sh`ul};mCy7bZAs31({h{&h{j9R5h0vai7^i)7*8P{g?lLvyCqei+aTX}f$p`ul z>DvJ4Tzu3M8!KgC>TGFtar#P8I2X$~9!f$HU6a;bI*_2n-&gdUs32#`$O?@CabGy> zxDrf)C6<5qD&Li>ImuL5qBOr9%Q2^^@kkpQN=|l@bY*S7-iXO&H-Ez{cQ0vI-IL6R zRM;%GR9E7##5cmXtG;D`?Ljz(Q;*C4iBZ7|RF3TT(UF}529oGBQi~_na_Pd)TS9;;5Yb<7s4vO3s^!?a}@Skc?{|57aa}{fr2xkp;Z}5S^7lko$b$O#A3;djZ465J2H3Rw4a@u18XHqRWDIO( zXuDC%0gX+sQOI-#^$LBv6;rMedbR?A@)jYSpRb9D$uh~qdS!?=(lCpl7@KFRtu2WA z{@27Fe|%zWU7CKv>f_@h#Hy@W9@1etG}74{w6LkDLbiwRB^%S_W7Ikrp!O zK^=s6Ge^JUzh>{KNbQ%+hJ4LXDTF2X02p5rkCl~Q(qpPygxpnpBDQv7_%LWEF7c^T zf(u2VaL^NQ1RxUI?<)#F1Ze|snfX3M6T7~p&A+~=i`vFIY3Wr}p* z@OYORa?VvZ;rs2OMag-B=%ztNSy)&Ie61o`~Z zZ2(Q@E;4h-YuMY7(Yjb!p!ICdnv(NCJJ$5RTWY#-%&u7&)c2Gyes>zGW6zPydoVut zmE@k{0JBSQ8Ae-oy%=kdj3=u<*m_?M%ZZfMGI};wV>v~fHnjJbh@$u{Z`GV$eD5Eh zGzMDFEagCefERvLCs1n*N!D`s*lUuUt)OH63KlV-wAwtm0@y^&!De0PyjX7d?ZrXO z$d5Kn#Y02q1jOmw)Khx+mdh#Ih#L z*ZyeVH*jv{vDg`SOW(FK5@;D6s?^8egc%L#m+#NlzT%t{31+@sn&-PJ3uZb!DcsgR z0Ud)BX@CVyox49QtGI!Tu>53@$=Y7A+*hl-OV%eE2FH}`ZX z$C?-=bk}S*185yxM_@KI^d4WFsqW8IjJ(Yhy#L1EEeqv8dcq)=o5LJ|R+1rnsxh6c z#xXu4TY2*u;+zX;Yz-kah)=x(S6YMR;BIuZxJ0S6qp|vuRJl7^^;;QBuc<#-6SdQ8 z=Y&m)xVQ}|zx=pooXod=m4mo=&tWtwm%T%-dT$Oz%r8%tT`M~m?#T^#`y)2AcwO5- z@$npxguN&Fp{A5omK~$dk!ud{GGYG{8YOWqp)x}mx2GdO--@frKq4bhOs+qoYFsu_;U5CS?e*(-)tusWVF=obBRr?Ko!4Wrb{gToF~MyMERTQ$@-+2#;Nx&G2TyYDE{L!y}{8kvOCWe zKlKR#G!L+;MN0-V1w1kk3vES74BYvaq+~UpBB6&P&UfpW zHr92k;cbs}xH`KNHbRI_UY#tQMbI=X&;Sj`F5UqfAe$_2$M>;a+L=bpYe*R?91U05r$W05{e=?GJpghLs9tsqWeuoK;pv zs|F4o8$-Op^!5)%JkM8y6c>T)XTCCx8}W-}?d9^^F%B-b6uhKjXw*!&W%* zu4>j2_Crlx9D*YB!P-nn8zig?xkCy;z0=#W&l)*l9f@~3GT@L`pa3BxNu%-c9WSe@ zs^$$0maVcksfzgl;o#5Bz>s1~AN8>9Rnehi(ny`>I_M}H*K=mP=+tZzyv4C#W5$_G z48QAeeKD{0xPr5vNOQa1f_K7Ph;^4Oh`+nq?r!H$+s7_^V0B6OLbSguV<#D$zLNl2 zXasoVzj?ws!KA7`Ks^#;+2L)ToVWKT4|KM=t@f3#sq3fL{d3P zJ;#1|@|!(C$hBJ|?Aq@fXkvTnO+NFkl01j@lg&CdI9z?05)ZATjJK>VoXG?fGLd8? zvqmw?UE;3*G&P}AVR=Eib%>mN_JA)KmJYn?PO zKhNU{Z=~k;rZcqaaKI{1!A9O(**X&tgbc|}3?SUU`V9^;T&wSoSI5uI(Q2ASL^CY1 zVwP4Z9=7m3lhoAF50s}xs6B7oyJHT92nh-K4QtsCI_y@e;Z&3O$-Px(%Y>>#tmjMb-dYN-J-nni~KS8-leMs$u*HNsk~={+#5`E>)+p1&%9QlCvTAvw2Whk ztM{RuR-Vv9#+z*UoUmW3zO`n2(OVVD9PV>vQ&UDHYa;`Y1m~wbjK^UsqPc$a`Yy2I z0e@!*G4I20owaj?BrftevmnId?Lzx#wj=#@K8eUX4XBJ&ix1kpvB5oHrjZoTxI30M z@@)*s`s0crK;(3$@PGs}Ew3F9~OEoEh6yq*Us?EwBJF7saMsc{v#?X}F_I#*o1eNodz-!$`Or%ci{mA1~TI3+kU{Pved08`Rb JtdqA4{|^CbEnWZs literal 8320 zcmbVybyO5@{O;2AMM_#iS{kHNLJ*{+8)T(f5Rh&V=};t>Zdd_ zaDVsQzwf!{%-J)$XXl-FKKVS)XQH$;lnL=^@IfFDp~@RYZ4d~}3ON6Oiw%6XxJitI zK#U+2MLAvX%>Ar@PbTYGo9*9zP`*vEo=tI5_QX}e{mdd(otjt^5MmuM`}v?_8~!xy zeMz_ohP7(*%Mw;6$!5vLA-qo#k|pnFG+q>Cjq!|R4-KRjf0|FHx10+BfyH~De_>(i ztZ`B?sG7RntqqcDiu$o2`sW)sVtF&uFLTSM)xe4dv&Gjb+tM=brkj60H-`L1$c zvE4|FDfa_gUzgZV;a(?C?c_VXMA*1ftF@D5;bt^_YMvo4tK0h6vKnSUo*+z2-%SHk zj+hck{!TqJ5^q=#Spc$}qP-u+Rz3_n_h4!~%etRfx!hw*638BIZR}s49leMm0d9ak z#~G=RzIOkL?L2MqgCH*jepTEB(f&I}cTSSP8q@bVBfGid+n<&f_qHB%=h@y6V^-ta zK^ZTw1C9e74G@Vc;e9wt3vu;1Pf_Cm6ub3yywK&5ebk$T#?#IHG?a^IA8|dhn-?_Y)-fs(8lCyC1?Vi8sJ#VZzTRtk5=a}&hKhaG6WmNY4G$wpu zm@TwT3%02W+>^#`Hri14g)S8sJL`bmB(qY|p}+sfN*XBzTfLYWv?;cj6{6W2jt{cXbbHHPZ_CmT`0g%iD(;^tTW!y1kbCMM zl-oF96-?K!_Nv61;gs>8plZ!Y;qtB{bLU2g$$Le}^{nEVR30D- z^R)RX(W~b%d8?lSd@4=nyrpRT3zn_w@Mqyc3(`_!dxnqBPx^v+{CIJj_I<25PS$q~ zQ6P|VG}+s$$Log1%JGzH><5Zh`quWM@3y-YqWmvlS#(MY<%a(D%FD>O3K}RQt`+MD zOXSOO6K4!P$fFD^E#pmFBt6=^{ZmvZV)#p4Fd_gSFc2UkDB}| zE|{C3*#U7l?miLW zR$zjDX=LG3o2gY@rp9M37iERvsp$&Yp>E~~4fZbb;;%2>%P^?%9T@DqG~skcA##UH z>W1gqHnoo<3U8!H2$JhEHNzs31t>=O>->zRcsA%=m}#(anSFz z`nh&tz#i*6hX2u(^ez5k2I?_UYpBKiNy8kXZ^K*B;dKr#1%YoXZ&4lJigf=nqS{51 z_Lgmm*q83CR5y(rNkQhIPXOjksOaK}AX}drL4T7~4O;Bl!veU#i*qFwHTAga2 zp~;zzhLaOdBVq7(Yswtq%Gq+{kf*J`f2ms1|8wa;DmSIsIjrEav!c~`RZZxqcJ_nX zq%gnU@7H=b?2!bGRewBRgBo`%Sg|wqavHh~nIaeUZ^?B2g&Wj~EY0m67`k}TA7KZ$ z7R6PK-sefDTfJX?_gj9DDn4*z?>}HR)pGbZ9gry?Q?~o2>(j3 z#$}c2PBgvy=CD`Ol!t~gI-f&zC>2nrMI>(y&w-7wW(jrF`h22^YX&km@8a}j42j4G zcPBPc+kCwHtX^MDv>ALP9|%Qb`WqXhSRSN_c7%S#+CVlO`0*b~9OM!z`GxqQ4 z^|bN$r?Vu@tMrY*$Dk40YUu*qw&+GQlrl92F%B`7SHff$HVE3$dLxg73GhJf!_}R<4gtc`Rstz~ zWoa3ZY}xe#Rq!IHvT|q=_f=`U2WCeMO)ol#=khkTx9xkWkf8-pCHgEj(hal=`V_KT zkR%pl#tu@m-y6rRln{9HeGtRj>^*Ns_~u6)4-Hk0bCZ7aj|f-RA^G{n1GIKZ(NI~t zo`6!!V8*MkbB?Qk{A3=ai=sA);Ofu_pZV%A%c6PptEE8tuvwMO=7p!w*`7RS-mFZT zK<~*qDHL}o>)YWUe(v8!&i*3R|b-rn57s0jR>{tR#UPMk4SlqLFHsb^x%soUU z`HSjQu-Duw)~t;GAk|^ch8ir>JL4Ezy-5w$eA#=DV&|UFg~E>&0M`w@r5V~}pqBNj zK9r8F&aty&KN*g#CVDeaeSyn4P?sR!$*{WlqZhaqs@IDS-Xoy7i8kmn$1~A+3Etz1 zT_!X3Nw=>x^5QuA2P^a90`C#8UUvU+NUyp%Or-+P!_MLUmso_SG*jN$+LvDY$Bf`T z!nlBHheZsU4V>PYY4V#!n6WLe)@f`tgTTF?%&#GfW>G3IPAcw-Q(411`w{J;J-goO zmBpSdQrpB&0`2Zh{Io9coEw=0Wr`*_XYU z_;>TOW8trORC;H~CQo%0T9lQAha6#DnG*-^zT$$__{IY<#xFjmfR%ndYiF6yki`uu zeE;l_l+KkU^?v40gI0M}cL;ISy&XvTYfv_M`UO>jDpUN?#ktr|?SiG%oSi{u#FFOtvkhbJQ(>Jc77}vg z{omlp9b2!k{GG62v`Z)~Y)!1ZI%jaQNi(cn36Vh*+`hugKK4%?N&7`VT*B?rf-+b$ zET@E~b7%QQCgQ($@*2Wy`K%mc?uq(`|Ahtd8S_{aHsGU7>xE|T13)sxc}vi@nAN}R zuFuJ(d0RPzfF#e-u8^xk2?pZQ4_kQf=mvM}#s^p=CS!Z>&4u>CIoODMZL&qs-D41YzO+sD6yk@};%R1AD2m+(alz2cPjs0%m=vsH{ql6q%U) zh^0h}i$7DPSlw>v>b*0M^;{se1lG53?OrAURFDUtHaGWd-!xD)@+6vnP<_JGU7k+| zQx#tTIqPvexTv2sqM35Y;TMLXxA@%4{?*MGpKjvFg3TqgSjMRo`?6znVC6?!(CsN?TsTe-26M^G%-RhhU- zGw5;n(R3{H6u2-q9NjvCN?>=wE2eOZA}JgsUnkvb8Jt&cu49%tgJW2M36TS6-mi<| zXcuafqL;_`Hy)C&QKvL>GFX9&_qf3nT(Dt>(s6Dj_3Z-~SAPNn`G$*`ZEcVS=g%7( z<%bfG|6kgZ92#Kjbq-&EU|f)Z-np+Hyq~o^csbhf9y;3MvLNkWMqnyF#~K-g)mGz& z)zOS(Ged;Tca)bYXrNuuU%}3yli~;1)6Zc=m2;rBIs4BX4fb=p$c>NIA755saPBMV z{SBf-N!{mmUQd8AEnYmxGATfO~a=h`v?MTi2vD7H{%XIQh@UF%k zs0Or$EKWJ!JT?0$T}FBs0l!|k6&_giBh8u3JBU4f$1UvR{|qJbfD05vt$wy-K_EaQ~vxfK=yD4uYp`-o<1WgqxK^k%zMhQ-A6OmFJ8dX z>2;K>HMf|`%yD{+=*%)*+X|^y&teEY%zhvV{?l8`_U8$QWNN8(qK{7A-e=W$r7*%) z^m~!hO{9y8@#%gK2ql{=A^O%|1lKZERkCrd*lgO z(W-T^th{n&{bUc<`r9pYeOHxdSL;wea9 zIhKC&%_lgq51vqiWQa51tXa@5hqqYP>drevNf}}W#RWVwoo*)T2$DZ#)1D56anPr7 z=$Me)DTCg>iFl#aj~XqLsbl44AKhL?zZ%A0cm6mGH-28kN*0}IZm{394?1GxZE+_5 zy$gGtArN9Vp=}@aaj$kJTb!QI-(qmRA!Vm`2GR2TfwJhES5w6Wd-=P?ctc5v%8I5} zIR#oTRx9jEEd@TI?lh()X1V)o|IBoH%Ut#i{s;yLHMrUIX;^bOx4}`>nMv&2-9o7) z`jzt?3Fv{&v?M^f;U1-+WidE)V*Q4|N_Yv!U7+og74aHtkp z9y8y$D!V|y%MItpy*9*3#aD$|c7#)qI>ghfUMxF)BfzYdE76Q4j6EA*11AWrNaGxh zB%Ao*xQKHO*eawK7N*AnnEulvDK5BE@B_Gk*Y8;jj1FDze{bpyGj;oEv^C2OAHPm= zWmU!##xLQ}+P@|4XThi>YQdgUUlxHwL|_<3df;T^zBrlE-3UI2T;>aM&**p}VCNC9 z4;x?kRzJT*0u`MC;QSR)Bbimy%F(q^TY11E*~5djnv`0PTI4L`tA16EW>_8peWofi z)iNe8@rxo)I|1Zk{K8YSFh2RB8zuZ*R1L?~To(44C$A@gw z_6V;vjpB+oZIg~|S_)};KoHq-8#?B-hVQ1C^C86p{E(G~FA{@J8I+wKRfEqe+8*GH zw(E>czPd|=+J`Ta-FVyzW_PFRKm2;@9L$?^E$kSfB5Jq0Btu3>4pCa2o}SMlqWyH! zoi<-9o;`dZT~BpaJvGw}GBEgWuF1uRxg`-n6Q^lFC!4|#jivj~R{Ju6Fjj0~s+R(% z%nI$0g{dljsz^bCge+T_<_wY0C@lfn8lmkX^jLT{{R*lzL1WuMVGQ9~)#1>oWwP6> z^Kd9-wl&M|z6ANB3I3teZJ?U%&%r)o$6ue;eJJlq)fyLt9pg8QSm;edq8KZ6)9SQd z{3;a9b#V6Vpgmh`@7Z3Owiaw(03V=ygkw(f{EJZOO8OO(arJ%}+dY2h>AG#&FE-D8 z!y2c@?9E-gcBba*7OLJJV+TaRZm^vJh9-ePk6#<}y7?AWva^`U6)Rl(atSv;YdSpY zO*%%|_Nodompe|O!Q0DMW;oE~t3CarD`L&uYw|S?c#l*f{#upIN@|!V;&`&-X@f!f z!ubrg^ZVSjKbP~s*y}7i>iQa@F@GRu zg0PWX+`*p>wNC3EP*f<=Q0Now!&VxCbSE422(WUiIUO=Az{l%awwyU*LnAPS!sf{@ zLToD=22Rt!s;YTP5D!3N`_dQS$G$;G_XBzyx#gDaxN&XZIq?)(9A`~DaeCC-^ zajPh-2)$-Yv2CZuN|-lE$WZ7EahSI)4;n;5pBaE5_Z;(`dUjGf`cz{?HOsmWPr{U! zZiKG>UeO(uIvj>dCbGDi*I8l>FQ5WT`^JgW!{Cg8Mn#J@ARIIlUn#6fQB#-!UR2~j z8ARYCw?J^-!06y>6Kv5>{GHA_@IR!?kvEa5SSL7;Ti(RDq}bjr327?RlzUiUa89^j zxzA5W9@Y~f_~-6)drfpl552;QX^X7>f^fkU_T{m8V5Cogx%M_%7Eb-h$23>HgyDC_ z=Wl|mt}^f6?xnBo?z%@g(z-$`q$< zPAC-SoT2VHhriK*W&AQbQuTUy=b9YE%2k#S+^nw|wt2-BX3;#iyMY~q>lFf1gm)`B zXRwAER!!T_DbmnVxhmFd|4BMNg?`A#Sp-lV0Q}i^rMEKHy@j;g(j?oZWcW`?uZ+_5 zo&j(Pyn68OpOWZhN)-JLt*nE;`o?Jz;h9`@vPqxCUw?Wu%3fnZ9c~=fs;2f}f22E! z=mZa;Vv65keW*);xEwc>+B~#M!H5{cw8J5wmd@UCuh@_#!LY<)g7gdrnVzhC8&N4q zPx$mvpIq65(%t1W<@Z@XvU8e0OYbxpTYncbhfr1>&VZYg^UbU<@ztywG?qjlrUBlllsv3hr#q>5kISJuHPl?(^RJ% zS^2mY?oum38GZw}zh}hNmq}<^E3g*kha>rjGys{1!+;m^RCXC?_&?JJpfvkcKxLpk zXfjuBe$|TIII+xEm`Ntg!Ub%Jg3=Xca)Np!GliG$m zEe2rkuQoN1N6s-8j*=Yz+Ci{lOQF!WU4DN>lz@L)u5`YX%lGvK#l>SI?Lo>tDr_zz zloo(yGeX)~kpgHVo=j{~q8lfli~#Y|91L0LjX$M=v&y5W;C&o+pOb$h zT?I&9>Y<+g4Qbx&5Or|IIFi&_7EdLpXf!pC@XT6$cJ*a+mLiVlQlj!~-l6VSqQ2ud zn_UdlzdG6-BYAi}g;Su<=a(-w0B-&ki|gXcMsXcxbd{}Y+VF7E3en`Hmmsf{I7wO@ zUm;1e0jIR+LPH)QFM1@4NxJ!Ej+Jiql=kwEu708TIe!@!N!zNt(*VQ0Q z$0tgl9?>Zf8;kR8O1?#+H#ND=N?Yyk!Toprdl^9VoU2Dw@nVN8s|VVNt%`cl;z zSOAcAYA`}SftCtvAP7N2Iws%2>f0F}a_CszOx`2j!xb1zFULErFi)bX! zsja}84_VqF0oviH^Nz8}hTi}>h#YsCHH-6EI7-XMPz`v@Af10$LG3*b>Q7JjCd*}n z+(d(`kn)=`qz%s-LH8236b+p6JfJ4h0%SG5NZ4)>A<{iiGYDF-*VVIOdC$(8sj)lT zPy0L1KmP(rjnHps3wc2?kI=p2>ug#$9jimDww8(LOP4U{7uv}DFWW))UlK79g1Dj3 zdG!#SB-tnei_>%DW&8qE=f4w?y+JooIArd{Vnp1@`y<_VCLLM+naJmOg_R~$QsfiI zJI^%a=Z=-sgHUV}gKN3qSVu-+hLc#*KY9pg$*UO5lnZu%tf}x?KQQ;5W2AghEcjoC zB-lBjyv7g6Gup&`G99y!>@;l5VDa2gZ#4 z4be}Mvf5R~k(;%Cnxa%FWfnc|j?~!H=w!E|+Jb`BngjDz{iH@;K)rl#@-UKp6DLOU zn6uOCTNfpicRmV^#FuDnVm@N zfiY^+z32QQd;WXqFqtDZX?HhkOgn=+`!F-L_>uTh z36Kp8o6=Hs(?M_bxZK=Xr?^=nqux^J$#Dw;B6q}(3rL6>XOLc%pGm6(<0Ik*MnAK5 z#>_l+vqw7fPApSW|NH-gQ+Um$*>utryBqk1;@?%W%LiA`KvPe~M*&?J0f6`syGlvc zmQYz937FB{~pJXV(8txqIs^Hv?T*}(sYgjZ3U zST1GvJxh6ePYrlsh;8-3QZzlb%&M9iYH0<95(Dg{3IEtK2D3-*Sc1&z~HoGn~ZUa)SScil2uOahfxZ7x!b`0 z>;j}dc02Fo1Up?tMG{a+eX!cCIO>7m1jOCU!4FF;?4O6ro=d2OS^tbs!EG>{r?$V*6MP+1%|0_<5FO3g;WewuDc#hw}3NG2doMO z0X$a8b6G-0)JjLDDs9sb7!Ng()<6FG(aV?kTs2IUMItrOeuvwfkcpKX4lD{L+|Y&z zBUd2gfFW?xU`pGkYQseQIu&5({Xa)b@|`vCN`U0!y8!?9$N%>eQ&m)u3SGq*O%$gZ Qunb7$wT5D)yk+SB0^iX+pa1{> From 2c8f51940af73fe614bbdd3614348d1210d88a16 Mon Sep 17 00:00:00 2001 From: Aaron3S Date: Sat, 26 Nov 2022 03:13:06 +0800 Subject: [PATCH 06/65] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9ops=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/ops/api/job.py | 4 +++- apps/ops/models/job.py | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/ops/api/job.py b/apps/ops/api/job.py index 86a52f373..e668bcee0 100644 --- a/apps/ops/api/job.py +++ b/apps/ops/api/job.py @@ -16,7 +16,9 @@ class JobViewSet(OrgBulkModelViewSet): def get_queryset(self): query_set = super().get_queryset() - return query_set.filter(instant=False) + if self.action != 'retrieve': + return query_set.filter(instant=False) + return query_set def perform_create(self, serializer): instance = serializer.save() diff --git a/apps/ops/models/job.py b/apps/ops/models/job.py index f2e7eaa4b..3795a0455 100644 --- a/apps/ops/models/job.py +++ b/apps/ops/models/job.py @@ -91,6 +91,9 @@ class Job(JMSOrgBaseModel, PeriodTaskModelMixin): def create_execution(self): return self.executions.create() + class Meta: + ordering = ['date_created'] + class JobExecution(JMSOrgBaseModel): id = models.UUIDField(default=uuid.uuid4, primary_key=True) @@ -198,3 +201,6 @@ class JobExecution(JMSOrgBaseModel): except Exception as e: logging.error(e, exc_info=True) self.set_error(e) + + class Meta: + ordering = ['-date_created'] From 8a3bc51faa66edf49836e7103b316d2c21954851 Mon Sep 17 00:00:00 2001 From: Bai Date: Sat, 26 Nov 2022 09:52:04 +0800 Subject: [PATCH 07/65] =?UTF-8?q?=E8=A7=A3=E5=86=B3=20authentication=20?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=E6=96=87=E4=BB=B6=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{0015_auto_20221125_2240.py => 0016_auto_20221125_2240.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename apps/authentication/migrations/{0015_auto_20221125_2240.py => 0016_auto_20221125_2240.py} (95%) diff --git a/apps/authentication/migrations/0015_auto_20221125_2240.py b/apps/authentication/migrations/0016_auto_20221125_2240.py similarity index 95% rename from apps/authentication/migrations/0015_auto_20221125_2240.py rename to apps/authentication/migrations/0016_auto_20221125_2240.py index 7b1c073e8..92478a523 100644 --- a/apps/authentication/migrations/0015_auto_20221125_2240.py +++ b/apps/authentication/migrations/0016_auto_20221125_2240.py @@ -7,7 +7,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('authentication', '0014_auto_20221122_2152'), + ('authentication', '0015_alter_connectiontoken_login'), ] operations = [ From 0b802b1782468a3c390b2345714169a760e8bc18 Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Sat, 26 Nov 2022 19:07:12 +0800 Subject: [PATCH 08/65] perf: navigation page --- apps/common/utils/timezone.py | 20 ++++-- apps/jumpserver/api.py | 100 ++++++++++++++++++----------- apps/orgs/caches.py | 76 +++++++++++++++++----- apps/orgs/signal_handlers/cache.py | 14 ++-- 4 files changed, 146 insertions(+), 64 deletions(-) diff --git a/apps/common/utils/timezone.py b/apps/common/utils/timezone.py index 4b60008af..17e44b7bf 100644 --- a/apps/common/utils/timezone.py +++ b/apps/common/utils/timezone.py @@ -1,22 +1,21 @@ -import datetime - import pytz +from datetime import datetime, timedelta, timezone from django.utils import timezone as dj_timezone from rest_framework.fields import DateTimeField -max = datetime.datetime.max.replace(tzinfo=datetime.timezone.utc) +max = datetime.max.replace(tzinfo=timezone.utc) -def astimezone(dt: datetime.datetime, tzinfo: pytz.tzinfo.DstTzInfo): +def astimezone(dt: datetime, tzinfo: pytz.tzinfo.DstTzInfo): assert dj_timezone.is_aware(dt) return tzinfo.normalize(dt.astimezone(tzinfo)) -def as_china_cst(dt: datetime.datetime): +def as_china_cst(dt: datetime): return astimezone(dt, pytz.timezone('Asia/Shanghai')) -def as_current_tz(dt: datetime.datetime): +def as_current_tz(dt: datetime): return astimezone(dt, dj_timezone.get_current_timezone()) @@ -36,6 +35,15 @@ def local_now_date_display(fmt='%Y-%m-%d'): return local_now().strftime(fmt) +def local_zero_hour(fmt='%Y-%m-%d'): + return datetime.strptime(local_now().strftime(fmt), fmt) + + +def local_monday(): + zero_hour_time = local_zero_hour() + return zero_hour_time - timedelta(zero_hour_time.weekday()) + + _rest_dt_field = DateTimeField() dt_parser = _rest_dt_field.to_internal_value dt_formatter = _rest_dt_field.to_representation diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py index 59580d178..bfa50da4d 100644 --- a/apps/jumpserver/api.py +++ b/apps/jumpserver/api.py @@ -3,45 +3,51 @@ import time from django.core.cache import cache from django.utils import timezone from django.utils.timesince import timesince -from django.db.models import Count, Max +from django.db.models import Count, Max, F from django.http.response import JsonResponse, HttpResponse from rest_framework.views import APIView from rest_framework.permissions import AllowAny -from collections import Counter +from rest_framework.request import Request from rest_framework.response import Response from users.models import User from assets.models import Asset +from assets.const import AllTypes from terminal.models import Session from terminal.utils import ComponentsPrometheusMetricsUtil from orgs.utils import current_org from common.utils import lazyproperty +from common.utils.timezone import local_now, local_zero_hour from orgs.caches import OrgResourceStatisticsCache - __all__ = ['IndexApi'] class DatesLoginMetricMixin: + request: Request + @lazyproperty def days(self): query_params = self.request.query_params - if query_params.get('monthly'): - return 30 - return 7 + # monthly + count = query_params.get('days') + return count if count else 0 @lazyproperty def sessions_queryset(self): - days = timezone.now() - timezone.timedelta(days=self.days) - sessions_queryset = Session.objects.filter(date_start__gt=days) + days = self.days + if days == 0: + t = local_zero_hour() + else: + t = local_now() - timezone.timedelta(days=days) + sessions_queryset = Session.objects.filter(date_start__gte=t) return sessions_queryset @lazyproperty def session_dates_list(self): - now = timezone.now() + now = local_now() dates = [(now - timezone.timedelta(days=i)).date() for i in range(self.days)] dates.reverse() - # dates = self.sessions_queryset.dates('date_start', 'day') return dates def get_dates_metrics_date(self): @@ -63,7 +69,7 @@ class DatesLoginMetricMixin: def __set_data_to_cache(self, date, tp, count): cache_key = self.get_cache_key(date, tp) - cache.set(cache_key, count, 3600*24*7) + cache.set(cache_key, count, 3600 * 24 * 7) @staticmethod def get_date_start_2_end(d): @@ -162,40 +168,45 @@ class DatesLoginMetricMixin: def dates_total_count_disabled_assets(self): return Asset.objects.filter(is_active=False).count() - # 以下是从week中而来 - def get_dates_login_times_top5_users(self): - users = self.sessions_queryset.values_list('user_id', flat=True) - users = [ - {'user': user, 'total': total} - for user, total in Counter(users).most_common(5) - ] - return users - def get_dates_total_count_login_users(self): return len(set(self.sessions_queryset.values_list('user_id', flat=True))) def get_dates_total_count_login_times(self): return self.sessions_queryset.count() - def get_dates_login_times_top10_assets(self): + @lazyproperty + def get_type_to_assets(self): + result = Asset.objects.annotate(type=F('platform__type')). \ + values('type').order_by('type').annotate(total=Count(1)) + all_types_dict = dict(AllTypes.choices()) + result = list(result) + for i in result: + tp = i['type'] + i['label'] = all_types_dict[tp] + return result + + def get_dates_login_times_assets(self): assets = self.sessions_queryset.values("asset") \ - .annotate(total=Count("asset")) \ - .annotate(last=Max("date_start")).order_by("-total")[:10] + .annotate(total=Count("asset")) \ + .annotate(last=Max("date_start")).order_by("-total") + assets = assets[:10] for asset in assets: asset['last'] = str(asset['last']) return list(assets) - def get_dates_login_times_top10_users(self): + def get_dates_login_times_users(self): users = self.sessions_queryset.values("user_id") \ - .annotate(total=Count("user_id")) \ - .annotate(user=Max('user')) \ - .annotate(last=Max("date_start")).order_by("-total")[:10] + .annotate(total=Count("user_id")) \ + .annotate(user=Max('user')) \ + .annotate(last=Max("date_start")).order_by("-total") + users = users[:10] for user in users: user['last'] = str(user['last']) return list(users) - def get_dates_login_record_top10_sessions(self): - sessions = self.sessions_queryset.order_by('-date_start')[:10] + def get_dates_login_record_sessions(self): + sessions = self.sessions_queryset.order_by('-date_start') + sessions = sessions[:10] for session in sessions: session.avatar_url = User.get_avatar_url("") sessions = [ @@ -229,11 +240,13 @@ class IndexApi(DatesLoginMetricMixin, APIView): if _all or query_params.get('total_count') or query_params.get('total_count_users'): data.update({ 'total_count_users': caches.users_amount, + 'total_count_users_this_week': caches.new_users_amount_this_week, }) if _all or query_params.get('total_count') or query_params.get('total_count_assets'): data.update({ 'total_count_assets': caches.assets_amount, + 'total_count_assets_this_week': caches.new_assets_amount_this_week, }) if _all or query_params.get('total_count') or query_params.get('total_count_online_users'): @@ -246,6 +259,23 @@ class IndexApi(DatesLoginMetricMixin, APIView): 'total_count_online_sessions': caches.total_count_online_sessions, }) + if _all or query_params.get('total_count') or query_params.get('total_count_today_failed_sessions'): + data.update({ + 'total_count_today_failed_sessions': caches.total_count_today_failed_sessions, + }) + if _all or query_params.get('total_count') or query_params.get('total_count_today_login_users'): + data.update({ + 'total_count_today_login_users': caches.total_count_today_login_users, + }) + if _all or query_params.get('total_count') or query_params.get('total_count_today_active_assets'): + data.update({ + 'total_count_today_active_assets': caches.total_count_today_active_assets, + }) + if _all or query_params.get('total_count') or query_params.get('total_count_type_to_assets_amount'): + data.update({ + 'total_count_type_to_assets_amount': self.get_type_to_assets, + }) + if _all or query_params.get('dates_metrics'): data.update({ 'dates_metrics_date': self.get_dates_metrics_date(), @@ -274,24 +304,19 @@ class IndexApi(DatesLoginMetricMixin, APIView): 'dates_total_count_login_times': self.get_dates_total_count_login_times(), }) - if _all or query_params.get('dates_login_times_top5_users'): - data.update({ - 'dates_login_times_top5_users': self.get_dates_login_times_top5_users(), - }) - if _all or query_params.get('dates_login_times_top10_assets'): data.update({ - 'dates_login_times_top10_assets': self.get_dates_login_times_top10_assets(), + 'dates_login_times_top10_assets': self.get_dates_login_times_assets(), }) if _all or query_params.get('dates_login_times_top10_users'): data.update({ - 'dates_login_times_top10_users': self.get_dates_login_times_top10_users(), + 'dates_login_times_top10_users': self.get_dates_login_times_users(), }) if _all or query_params.get('dates_login_record_top10_sessions'): data.update({ - 'dates_login_record_top10_sessions': self.get_dates_login_record_top10_sessions() + 'dates_login_record_top10_sessions': self.get_dates_login_record_sessions() }) return JsonResponse(data, status=200) @@ -353,4 +378,3 @@ class PrometheusMetricsApi(HealthApiMixin): util = ComponentsPrometheusMetricsUtil() metrics_text = util.get_prometheus_metrics_text() return HttpResponse(metrics_text, content_type='text/plain; version=0.0.4; charset=utf-8') - diff --git a/apps/orgs/caches.py b/apps/orgs/caches.py index a17cd832b..6b73c43b8 100644 --- a/apps/orgs/caches.py +++ b/apps/orgs/caches.py @@ -1,11 +1,14 @@ from django.db.transaction import on_commit + from orgs.models import Organization from orgs.tasks import refresh_org_cache_task from orgs.utils import current_org, tmp_to_org - from common.cache import Cache, IntegerField from common.utils import get_logger +from common.utils.timezone import local_zero_hour, local_monday from users.models import UserGroup, User +from audits.models import UserLoginLog +from audits.const import LoginStatusChoices from assets.models import Node, Domain, Asset, Account from terminal.models import Session from perms.models import AssetPermission @@ -35,30 +38,35 @@ class OrgRelatedCache(Cache): """ 在事务提交之后再发送信号,防止因事务的隔离性导致未获得最新的数据 """ + def func(): logger.debug(f'CACHE: Send refresh task {self}.{fields}') refresh_org_cache_task.delay(self, *fields) + on_commit(func) def expire(self, *fields): def func(): super(OrgRelatedCache, self).expire(*fields) + on_commit(func) class OrgResourceStatisticsCache(OrgRelatedCache): users_amount = IntegerField() - groups_amount = IntegerField(queryset=UserGroup.objects) - assets_amount = IntegerField() + new_users_amount_this_week = IntegerField() + new_assets_amount_this_week = IntegerField() nodes_amount = IntegerField(queryset=Node.objects) - accounts_amount = IntegerField(queryset=Account.objects) domains_amount = IntegerField(queryset=Domain.objects) - # gateways_amount = IntegerField(queryset=Gateway.objects) + groups_amount = IntegerField(queryset=UserGroup.objects) + accounts_amount = IntegerField(queryset=Account.objects) asset_perms_amount = IntegerField(queryset=AssetPermission.objects) - total_count_online_users = IntegerField() total_count_online_sessions = IntegerField() + total_count_today_login_users = IntegerField() + total_count_today_active_assets = IntegerField() + total_count_today_failed_sessions = IntegerField() def __init__(self, org): super().__init__() @@ -70,18 +78,56 @@ class OrgResourceStatisticsCache(OrgRelatedCache): def get_current_org(self): return self.org + def get_users(self): + return User.get_org_users(self.org) + + @staticmethod + def get_assets(): + return Asset.objects.all() + def compute_users_amount(self): - amount = User.get_org_users(self.org).count() - return amount + users = self.get_users() + return users.count() + + def compute_new_users_amount_this_week(self): + monday_time = local_monday() + users = self.get_users().filter(date_joined__gte=monday_time) + return users.count() def compute_assets_amount(self): - if self.org.is_root(): - return Asset.objects.all().count() - node = Node.org_root() - return node.assets_amount + assets = self.get_assets() + return assets.count() - def compute_total_count_online_users(self): - return Session.objects.filter(is_finished=False).values_list('user_id').distinct().count() + def compute_new_assets_amount_this_week(self): + monday_time = local_monday() + assets = self.get_assets().filter(date_created__gte=monday_time) + return assets.count() - def compute_total_count_online_sessions(self): + @staticmethod + def compute_total_count_online_users(): + return Session.objects.filter( + is_finished=False + ).values_list('user_id').distinct().count() + + @staticmethod + def compute_total_count_online_sessions(): return Session.objects.filter(is_finished=False).count() + + @staticmethod + def compute_total_count_today_login_users(): + t = local_zero_hour() + return UserLoginLog.objects.filter( + datetime__gte=t, status=LoginStatusChoices.success + ).values('username').distinct().count() + + @staticmethod + def compute_total_count_today_active_assets(): + t = local_zero_hour() + return Session.objects.filter( + date_start__gte=t, is_success=False + ).values('asset_id').distinct().count() + + @staticmethod + def compute_total_count_today_failed_sessions(): + t = local_zero_hour() + return Session.objects.filter(date_start__gte=t, is_success=False).count() diff --git a/apps/orgs/signal_handlers/cache.py b/apps/orgs/signal_handlers/cache.py index b3e06362d..9d3ce5c85 100644 --- a/apps/orgs/signal_handlers/cache.py +++ b/apps/orgs/signal_handlers/cache.py @@ -2,8 +2,9 @@ from django.db.models.signals import post_save, pre_delete, pre_save, post_delet from django.dispatch import receiver from orgs.models import Organization -from assets.models import Node +from assets.models import Node, Account from perms.models import AssetPermission +from audits.models import UserLoginLog from users.models import UserGroup, User from users.signals import pre_user_leave_org from terminal.models import Session @@ -74,12 +75,15 @@ def on_user_delete_refresh_cache(sender, instance, **kwargs): class OrgResourceStatisticsRefreshUtil: model_cache_field_mapper = { - AssetPermission: ['asset_perms_amount'], - Domain: ['domains_amount'], Node: ['nodes_amount'], - Asset: ['assets_amount'], + Domain: ['domains_amount'], UserGroup: ['groups_amount'], - RoleBinding: ['users_amount'] + Account: ['accounts_amount'], + UserLoginLog: ['total_count_today_login_users'], + RoleBinding: ['users_amount', 'new_users_amount_this_week'], + Asset: ['assets_amount', 'new_assets_amount_this_week'], + AssetPermission: ['asset_perms_amount'], + } @classmethod From 392ae18d85f0e5f56b0fd4ad34d7f635053bf032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E5=B0=8F=E7=99=BD?= <296015668@qq.com> Date: Sun, 27 Nov 2022 05:56:53 +0800 Subject: [PATCH 09/65] =?UTF-8?q?feat:=20=E4=BD=BF=E7=94=A8=20uvicorn=20?= =?UTF-8?q?=E5=8F=96=E4=BB=A3=20daphne?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../management/commands/services/command.py | 8 +----- .../commands/services/services/__init__.py | 1 - .../commands/services/services/daphne.py | 25 ------------------- .../commands/services/services/gunicorn.py | 4 +-- requirements/requirements.txt | 4 +-- 5 files changed, 5 insertions(+), 37 deletions(-) delete mode 100644 apps/common/management/commands/services/services/daphne.py diff --git a/apps/common/management/commands/services/command.py b/apps/common/management/commands/services/command.py index dd2cd9cdb..1fb28fd3a 100644 --- a/apps/common/management/commands/services/command.py +++ b/apps/common/management/commands/services/command.py @@ -6,7 +6,6 @@ from .hands import * class Services(TextChoices): gunicorn = 'gunicorn', 'gunicorn' - daphne = 'daphne', 'daphne' celery_ansible = 'celery_ansible', 'celery_ansible' celery_default = 'celery_default', 'celery_default' beat = 'beat', 'beat' @@ -22,7 +21,6 @@ class Services(TextChoices): from . import services services_map = { cls.gunicorn.value: services.GunicornService, - cls.daphne: services.DaphneService, cls.flower: services.FlowerService, cls.celery_default: services.CeleryDefaultService, cls.celery_ansible: services.CeleryAnsibleService, @@ -30,13 +28,9 @@ class Services(TextChoices): } return services_map.get(name) - @classmethod - def ws_services(cls): - return [cls.daphne] - @classmethod def web_services(cls): - return [cls.gunicorn, cls.daphne, cls.flower] + return [cls.gunicorn, cls.flower] @classmethod def celery_services(cls): diff --git a/apps/common/management/commands/services/services/__init__.py b/apps/common/management/commands/services/services/__init__.py index cceb9627c..35329a7d4 100644 --- a/apps/common/management/commands/services/services/__init__.py +++ b/apps/common/management/commands/services/services/__init__.py @@ -1,6 +1,5 @@ from .beat import * from .celery_ansible import * from .celery_default import * -from .daphne import * from .flower import * from .gunicorn import * diff --git a/apps/common/management/commands/services/services/daphne.py b/apps/common/management/commands/services/services/daphne.py deleted file mode 100644 index 09dd337a6..000000000 --- a/apps/common/management/commands/services/services/daphne.py +++ /dev/null @@ -1,25 +0,0 @@ -from ..hands import * -from .base import BaseService - -__all__ = ['DaphneService'] - - -class DaphneService(BaseService): - - def __init__(self, **kwargs): - super().__init__(**kwargs) - - @property - def cmd(self): - print("\n- Start Daphne ASGI WS Server") - - cmd = [ - 'daphne', 'jumpserver.asgi:application', - '-b', HTTP_HOST, - '-p', str(WS_PORT), - ] - return cmd - - @property - def cwd(self): - return APPS_DIR diff --git a/apps/common/management/commands/services/services/gunicorn.py b/apps/common/management/commands/services/services/gunicorn.py index bfaeea8c4..5cc67b45c 100644 --- a/apps/common/management/commands/services/services/gunicorn.py +++ b/apps/common/management/commands/services/services/gunicorn.py @@ -17,9 +17,9 @@ class GunicornService(BaseService): log_format = '%(h)s %(t)s %(L)ss "%(r)s" %(s)s %(b)s ' bind = f'{HTTP_HOST}:{HTTP_PORT}' cmd = [ - 'gunicorn', 'jumpserver.wsgi', + 'gunicorn', 'jumpserver.asgi:application', '-b', bind, - '-k', 'gthread', + '-k', 'uvicorn.workers.UvicornWorker', '--threads', '10', '-w', str(self.worker), '--max-requests', '4096', diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 8af2fcb0f..0ff982b81 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -86,8 +86,6 @@ pytz==2022.1 # Runtime django-proxy==1.2.1 channels-redis==3.4.0 -channels==3.0.4 -daphne==3.0.2 python-daemon==2.3.0 eventlet==0.33.1 greenlet==1.1.2 @@ -96,6 +94,8 @@ celery==5.2.7 flower==1.0.0 django-celery-beat==2.3.0 kombu==5.2.4 +uvicorn==0.20.0 +websockets==10.4 # Auth python-ldap==3.4.0 ldap3==2.9.1 From eec463774a70e426b26ed8ad920a7d3567a2fc34 Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Sun, 27 Nov 2022 12:53:38 +0800 Subject: [PATCH 10/65] perf: user login logs --- apps/orgs/caches.py | 11 +++++++---- apps/orgs/signal_handlers/cache.py | 1 - 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/orgs/caches.py b/apps/orgs/caches.py index 6b73c43b8..0a665c130 100644 --- a/apps/orgs/caches.py +++ b/apps/orgs/caches.py @@ -113,12 +113,15 @@ class OrgResourceStatisticsCache(OrgRelatedCache): def compute_total_count_online_sessions(): return Session.objects.filter(is_finished=False).count() - @staticmethod - def compute_total_count_today_login_users(): + def compute_total_count_today_login_users(self): t = local_zero_hour() - return UserLoginLog.objects.filter( + user_login_logs = UserLoginLog.objects.filter( datetime__gte=t, status=LoginStatusChoices.success - ).values('username').distinct().count() + ) + if not self.org.is_root(): + usernames = self.org.get_members().values('username') + user_login_logs = user_login_logs.filter(username__in=usernames) + return user_login_logs.values('username').distinct().count() @staticmethod def compute_total_count_today_active_assets(): diff --git a/apps/orgs/signal_handlers/cache.py b/apps/orgs/signal_handlers/cache.py index 9d3ce5c85..27947c2d2 100644 --- a/apps/orgs/signal_handlers/cache.py +++ b/apps/orgs/signal_handlers/cache.py @@ -79,7 +79,6 @@ class OrgResourceStatisticsRefreshUtil: Domain: ['domains_amount'], UserGroup: ['groups_amount'], Account: ['accounts_amount'], - UserLoginLog: ['total_count_today_login_users'], RoleBinding: ['users_amount', 'new_users_amount_this_week'], Asset: ['assets_amount', 'new_assets_amount_this_week'], AssetPermission: ['asset_perms_amount'], From 61c96baeaee6f18269d59b00aab21594dc6a012a Mon Sep 17 00:00:00 2001 From: Bai Date: Sun, 27 Nov 2022 14:36:16 +0800 Subject: [PATCH 11/65] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=8E=B7?= =?UTF-8?q?=E5=8F=96UserLoginLog=E5=AF=B9=E8=B1=A1org=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/orgs/signal_handlers/cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/orgs/signal_handlers/cache.py b/apps/orgs/signal_handlers/cache.py index 27947c2d2..505422377 100644 --- a/apps/orgs/signal_handlers/cache.py +++ b/apps/orgs/signal_handlers/cache.py @@ -91,7 +91,7 @@ class OrgResourceStatisticsRefreshUtil: if not cache_field_name: return OrgResourceStatisticsCache(Organization.root()).expire(*cache_field_name) - if instance.org: + if getattr(instance, 'org', None): OrgResourceStatisticsCache(instance.org).expire(*cache_field_name) From 23f3f903f55f478fbfd651f4e960c75ee1cb0a9e Mon Sep 17 00:00:00 2001 From: ibuler Date: Sun, 27 Nov 2022 18:31:28 +0800 Subject: [PATCH 12/65] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=20connect=20to?= =?UTF-8?q?ken?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/api/connection_token.py | 1 - apps/authentication/models/connection_token.py | 12 ++++++------ apps/authentication/serializers/connection_token.py | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 59b0b7593..a6e818cce 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -1,7 +1,6 @@ import base64 import json import os -import time import urllib.parse from django.http import HttpResponse diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py index d0a1d8478..4bfac68d2 100644 --- a/apps/authentication/models/connection_token.py +++ b/apps/authentication/models/connection_token.py @@ -77,7 +77,7 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel): def permed_account(self): from perms.utils import PermAccountUtil permed_account = PermAccountUtil().validate_permission( - self.user, self.asset, self.login + self.user, self.asset, self.account_name ) return permed_account @@ -100,13 +100,13 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel): is_valid = False error = _('No asset or inactive asset') return is_valid, error - if not self.login: + if not self.account_name: error = _('No account') raise PermissionDenied(error) if not self.permed_account or not self.permed_account.actions: msg = 'user `{}` not has asset `{}` permission for login `{}`'.format( - self.user, self.asset, self.login + self.user, self.asset, self.account_name ) raise PermissionDenied(msg) @@ -123,10 +123,10 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel): if not self.asset: return None - account = self.asset.accounts.filter(name=self.login).first() - if self.login == '@INPUT' or not account: + account = self.asset.accounts.filter(name=self.account_name).first() + if self.account_name == '@INPUT' or not account: return { - 'name': self.login, + 'name': self.account_name, 'username': self.username, 'secret_type': 'password', 'secret': self.secret diff --git a/apps/authentication/serializers/connection_token.py b/apps/authentication/serializers/connection_token.py index db6b35963..117ec2a04 100644 --- a/apps/authentication/serializers/connection_token.py +++ b/apps/authentication/serializers/connection_token.py @@ -154,7 +154,7 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin): class Meta: model = ConnectionToken fields = [ - 'id', 'secret', 'user', 'asset', 'account', + 'id', 'value', 'user', 'asset', 'account', 'protocol', 'domain', 'gateway', 'actions', 'expire_at', 'platform', ] From b2bb46a51e0cf203b4537188dc1db1d62c1adc4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E5=B0=8F=E7=99=BD?= <296015668@qq.com> Date: Sun, 27 Nov 2022 20:20:26 +0800 Subject: [PATCH 13/65] =?UTF-8?q?perf:=20=E5=8E=BB=E6=8E=89=E4=B8=8D?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E7=9A=84=208070=20=E7=AB=AF=E5=8F=A3?= =?UTF-8?q?=E6=A0=87=E8=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- Dockerfile.loong64 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 28dedbba9..e20279604 100644 --- a/Dockerfile +++ b/Dockerfile @@ -100,6 +100,6 @@ VOLUME /opt/jumpserver/logs ENV LANG=zh_CN.UTF-8 -EXPOSE 8070 EXPOSE 8080 + ENTRYPOINT ["./entrypoint.sh"] diff --git a/Dockerfile.loong64 b/Dockerfile.loong64 index 580792776..c2fa521b6 100644 --- a/Dockerfile.loong64 +++ b/Dockerfile.loong64 @@ -91,6 +91,6 @@ VOLUME /opt/jumpserver/logs ENV LANG=zh_CN.UTF-8 -EXPOSE 8070 EXPOSE 8080 + ENTRYPOINT ["./entrypoint.sh"] From 7f2267cf1338ac26e5bd6b4461644d59eae1e4ca Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Mon, 28 Nov 2022 11:42:03 +0800 Subject: [PATCH 14/65] perf: navigation page --- apps/jumpserver/api.py | 61 ------------------------------------------ 1 file changed, 61 deletions(-) diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py index bfa50da4d..b70983c39 100644 --- a/apps/jumpserver/api.py +++ b/apps/jumpserver/api.py @@ -133,47 +133,6 @@ class DatesLoginMetricMixin: data.append(count) return data - @lazyproperty - def dates_total_count_active_users(self): - count = len(set(self.sessions_queryset.values_list('user_id', flat=True))) - return count - - @lazyproperty - def dates_total_count_inactive_users(self): - total = current_org.get_members().count() - active = self.dates_total_count_active_users - count = total - active - if count < 0: - count = 0 - return count - - @lazyproperty - def dates_total_count_disabled_users(self): - return current_org.get_members().filter(is_active=False).count() - - @lazyproperty - def dates_total_count_active_assets(self): - return len(set(self.sessions_queryset.values_list('asset', flat=True))) - - @lazyproperty - def dates_total_count_inactive_assets(self): - total = Asset.objects.all().count() - active = self.dates_total_count_active_assets - count = total - active - if count < 0: - count = 0 - return count - - @lazyproperty - def dates_total_count_disabled_assets(self): - return Asset.objects.filter(is_active=False).count() - - def get_dates_total_count_login_users(self): - return len(set(self.sessions_queryset.values_list('user_id', flat=True))) - - def get_dates_total_count_login_times(self): - return self.sessions_queryset.count() - @lazyproperty def get_type_to_assets(self): result = Asset.objects.annotate(type=F('platform__type')). \ @@ -284,26 +243,6 @@ class IndexApi(DatesLoginMetricMixin, APIView): 'dates_metrics_total_count_active_assets': self.get_dates_metrics_total_count_active_assets(), }) - if _all or query_params.get('dates_total_count_users'): - data.update({ - 'dates_total_count_active_users': self.dates_total_count_active_users, - 'dates_total_count_inactive_users': self.dates_total_count_inactive_users, - 'dates_total_count_disabled_users': self.dates_total_count_disabled_users, - }) - - if _all or query_params.get('dates_total_count_assets'): - data.update({ - 'dates_total_count_active_assets': self.dates_total_count_active_assets, - 'dates_total_count_inactive_assets': self.dates_total_count_inactive_assets, - 'dates_total_count_disabled_assets': self.dates_total_count_disabled_assets, - }) - - if _all or query_params.get('dates_total_count'): - data.update({ - 'dates_total_count_login_users': self.get_dates_total_count_login_users(), - 'dates_total_count_login_times': self.get_dates_total_count_login_times(), - }) - if _all or query_params.get('dates_login_times_top10_assets'): data.update({ 'dates_login_times_top10_assets': self.get_dates_login_times_assets(), From 072c44974e231d6bb8c9803189ef0a8c118f0f26 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Mon, 28 Nov 2022 11:47:40 +0800 Subject: [PATCH 15/65] perf: navigation page (#9125) Co-authored-by: feng <1304903146@qq.com> --- apps/jumpserver/api.py | 61 ------------------------------------------ 1 file changed, 61 deletions(-) diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py index bfa50da4d..b70983c39 100644 --- a/apps/jumpserver/api.py +++ b/apps/jumpserver/api.py @@ -133,47 +133,6 @@ class DatesLoginMetricMixin: data.append(count) return data - @lazyproperty - def dates_total_count_active_users(self): - count = len(set(self.sessions_queryset.values_list('user_id', flat=True))) - return count - - @lazyproperty - def dates_total_count_inactive_users(self): - total = current_org.get_members().count() - active = self.dates_total_count_active_users - count = total - active - if count < 0: - count = 0 - return count - - @lazyproperty - def dates_total_count_disabled_users(self): - return current_org.get_members().filter(is_active=False).count() - - @lazyproperty - def dates_total_count_active_assets(self): - return len(set(self.sessions_queryset.values_list('asset', flat=True))) - - @lazyproperty - def dates_total_count_inactive_assets(self): - total = Asset.objects.all().count() - active = self.dates_total_count_active_assets - count = total - active - if count < 0: - count = 0 - return count - - @lazyproperty - def dates_total_count_disabled_assets(self): - return Asset.objects.filter(is_active=False).count() - - def get_dates_total_count_login_users(self): - return len(set(self.sessions_queryset.values_list('user_id', flat=True))) - - def get_dates_total_count_login_times(self): - return self.sessions_queryset.count() - @lazyproperty def get_type_to_assets(self): result = Asset.objects.annotate(type=F('platform__type')). \ @@ -284,26 +243,6 @@ class IndexApi(DatesLoginMetricMixin, APIView): 'dates_metrics_total_count_active_assets': self.get_dates_metrics_total_count_active_assets(), }) - if _all or query_params.get('dates_total_count_users'): - data.update({ - 'dates_total_count_active_users': self.dates_total_count_active_users, - 'dates_total_count_inactive_users': self.dates_total_count_inactive_users, - 'dates_total_count_disabled_users': self.dates_total_count_disabled_users, - }) - - if _all or query_params.get('dates_total_count_assets'): - data.update({ - 'dates_total_count_active_assets': self.dates_total_count_active_assets, - 'dates_total_count_inactive_assets': self.dates_total_count_inactive_assets, - 'dates_total_count_disabled_assets': self.dates_total_count_disabled_assets, - }) - - if _all or query_params.get('dates_total_count'): - data.update({ - 'dates_total_count_login_users': self.get_dates_total_count_login_users(), - 'dates_total_count_login_times': self.get_dates_total_count_login_times(), - }) - if _all or query_params.get('dates_login_times_top10_assets'): data.update({ 'dates_login_times_top10_assets': self.get_dates_login_times_assets(), From 0212e32ab22092a29010aa6a597a68f39e6b8c89 Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Mon, 28 Nov 2022 14:52:42 +0800 Subject: [PATCH 16/65] perf: navigation api --- apps/jumpserver/api.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py index b70983c39..c7dbb6cd7 100644 --- a/apps/jumpserver/api.py +++ b/apps/jumpserver/api.py @@ -199,15 +199,33 @@ class IndexApi(DatesLoginMetricMixin, APIView): if _all or query_params.get('total_count') or query_params.get('total_count_users'): data.update({ 'total_count_users': caches.users_amount, - 'total_count_users_this_week': caches.new_users_amount_this_week, }) if _all or query_params.get('total_count') or query_params.get('total_count_assets'): data.update({ 'total_count_assets': caches.assets_amount, + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_users_this_week'): + data.update({ + 'total_count_users_this_week': caches.new_users_amount_this_week, + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_assets_this_week'): + data.update({ 'total_count_assets_this_week': caches.new_assets_amount_this_week, }) + if _all or query_params.get('total_count') or query_params.get('total_count_today_login_users'): + data.update({ + 'total_count_today_login_users': caches.total_count_today_login_users, + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_today_active_assets'): + data.update({ + 'total_count_today_active_assets': caches.total_count_today_active_assets, + }) + if _all or query_params.get('total_count') or query_params.get('total_count_online_users'): data.update({ 'total_count_online_users': caches.total_count_online_users, @@ -222,14 +240,7 @@ class IndexApi(DatesLoginMetricMixin, APIView): data.update({ 'total_count_today_failed_sessions': caches.total_count_today_failed_sessions, }) - if _all or query_params.get('total_count') or query_params.get('total_count_today_login_users'): - data.update({ - 'total_count_today_login_users': caches.total_count_today_login_users, - }) - if _all or query_params.get('total_count') or query_params.get('total_count_today_active_assets'): - data.update({ - 'total_count_today_active_assets': caches.total_count_today_active_assets, - }) + if _all or query_params.get('total_count') or query_params.get('total_count_type_to_assets_amount'): data.update({ 'total_count_type_to_assets_amount': self.get_type_to_assets, From a1d72a17461fbe456e5fb69de7c66a9f5598b0a8 Mon Sep 17 00:00:00 2001 From: ibuler Date: Mon, 28 Nov 2022 15:01:16 +0800 Subject: [PATCH 17/65] =?UTF-8?q?pref:=20=E4=BF=AE=E6=94=B9=20connect=20to?= =?UTF-8?q?ken=20=E4=B8=80=E4=BA=9B=20Url?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/api/connection_token.py | 22 ++++++------------- .../authentication/models/connection_token.py | 20 ++++++++--------- apps/locale/ja/LC_MESSAGES/django.mo | 4 ++-- apps/locale/ja/LC_MESSAGES/django.po | 4 ---- apps/locale/zh/LC_MESSAGES/django.mo | 4 ++-- apps/locale/zh/LC_MESSAGES/django.po | 6 +---- 6 files changed, 22 insertions(+), 38 deletions(-) diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index a6e818cce..e08ba5291 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -159,7 +159,7 @@ class RDPFileClientProtocolURLMixin: 'ip': endpoint.host, 'port': str(endpoint.ssh_port), 'username': 'JMS-{}'.format(str(token.id)), - 'password': token.secret + 'password': token.value } token = json.dumps(data) return filename, token @@ -176,9 +176,9 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin): get_serializer: callable perform_create: callable - @action(methods=['POST', 'GET'], detail=False, url_path='rdp/file') - def get_rdp_file(self, request, *args, **kwargs): - token = self.create_connection_token() + @action(methods=['POST', 'GET'], detail=True, url_path='rdp-file') + def get_rdp_file(self, *args, **kwargs): + token = self.get_object() token.is_valid() filename, content = self.get_rdp_file_info(token) filename = '{}.rdp'.format(filename) @@ -186,9 +186,9 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin): response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename return response - @action(methods=['POST', 'GET'], detail=False, url_path='client-url') - def get_client_protocol_url(self, request, *args, **kwargs): - token = self.create_connection_token() + @action(methods=['POST', 'GET'], detail=True, url_path='client-url') + def get_client_protocol_url(self, *args, **kwargs): + token = self.get_object() token.is_valid() try: protocol_data = self.get_client_protocol_data(token) @@ -207,14 +207,6 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin): instance.expire() return Response(status=status.HTTP_204_NO_CONTENT) - def create_connection_token(self): - data = self.request.query_params if self.request.method == 'GET' else self.request.data - serializer = self.get_serializer(data=data) - serializer.is_valid(raise_exception=True) - self.perform_create(serializer) - token: ConnectionToken = serializer.instance - return token - class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelViewSet): filterset_fields = ( diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py index 4bfac68d2..2298eb0f3 100644 --- a/apps/authentication/models/connection_token.py +++ b/apps/authentication/models/connection_token.py @@ -1,17 +1,17 @@ -import time from datetime import timedelta + +from django.conf import settings +from django.db import models from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from django.db import models -from django.conf import settings from rest_framework.exceptions import PermissionDenied -from orgs.mixins.models import OrgModelMixin +from assets.const import Protocol +from common.db.fields import EncryptCharField +from common.db.models import JMSBaseModel from common.utils import lazyproperty, pretty_string from common.utils.timezone import as_current_tz -from common.db.models import JMSBaseModel -from common.db.fields import EncryptCharField -from assets.const import Protocol +from orgs.mixins.models import OrgModelMixin def date_expired_default(): @@ -21,7 +21,7 @@ def date_expired_default(): class ConnectionToken(OrgModelMixin, JMSBaseModel): value = models.CharField(max_length=64, default='', verbose_name=_("Value")) user = models.ForeignKey( - 'users.User', on_delete=models.SET_NULL, null=True, blank=True, + 'users.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='connection_tokens', verbose_name=_('User') ) asset = models.ForeignKey( @@ -29,8 +29,8 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel): related_name='connection_tokens', verbose_name=_('Asset'), ) account_name = models.CharField(max_length=128, verbose_name=_("Account name")) # 登录账号Name - input_username = models.CharField(max_length=128, default='', verbose_name=_("Input Username")) - input_secret = EncryptCharField(max_length=64, default='', verbose_name=_("Input Secret")) + input_username = models.CharField(max_length=128, default='', blank=True, verbose_name=_("Input Username")) + input_secret = EncryptCharField(max_length=64, default='', blank=True, verbose_name=_("Input Secret")) protocol = models.CharField( choices=Protocol.choices, max_length=16, default=Protocol.ssh, verbose_name=_("Protocol") ) diff --git a/apps/locale/ja/LC_MESSAGES/django.mo b/apps/locale/ja/LC_MESSAGES/django.mo index 3e19617aa..f74980a81 100644 --- a/apps/locale/ja/LC_MESSAGES/django.mo +++ b/apps/locale/ja/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:adfa9c01178d5f6490e616f62d41c71974d42f9e3bd078fcf1b3c7124384df0b -size 117024 +oid sha256:4a5338177d87680e0030c77f187a06664136d5dea63c8dffc43fa686091f2da4 +size 117102 diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po index dddb5b792..e9f747826 100644 --- a/apps/locale/ja/LC_MESSAGES/django.po +++ b/apps/locale/ja/LC_MESSAGES/django.po @@ -603,14 +603,10 @@ msgid "All" msgstr "すべて" #: assets/models/account.py:46 -#, fuzzy -#| msgid "Manually input" msgid "Manual input" msgstr "手動入力" #: assets/models/account.py:47 -#, fuzzy -#| msgid "Dynamic code" msgid "Dynamic user" msgstr "動的コード" diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 4451b285c..e3f454768 100644 --- a/apps/locale/zh/LC_MESSAGES/django.mo +++ b/apps/locale/zh/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eeaa813f4ea052a1cd85b8ae5addfde6b088fd21a0261f8724d62823835512a2 -size 104043 +oid sha256:30ae571e06eb7d2f0fee70013a812ea3bdb8e14715e1a1f4eb5e2c92311034f8 +size 104086 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 686c70a31..56b0a8f28 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -578,16 +578,12 @@ msgid "All" msgstr "全部" #: assets/models/account.py:46 -#, fuzzy -#| msgid "Manually input" msgid "Manual input" msgstr "手动输入" #: assets/models/account.py:47 -#, fuzzy -#| msgid "Dynamic code" msgid "Dynamic user" -msgstr "动态码" +msgstr "动态用户" #: assets/models/account.py:55 msgid "Su from" From 3c5b459ab79350c3d0ed3dcd6e14f850160bc395 Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 28 Nov 2022 15:31:00 +0800 Subject: [PATCH 18/65] fix: connect token serializer --- apps/authentication/models/connection_token.py | 2 +- apps/authentication/serializers/connection_token.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py index 2298eb0f3..82ad3589f 100644 --- a/apps/authentication/models/connection_token.py +++ b/apps/authentication/models/connection_token.py @@ -136,7 +136,7 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel): 'name': account.name, 'username': account.username, 'secret_type': account.secret_type, - 'secret': account.secret_type or self.secret + 'secret': account.secret or self.secret } @lazyproperty diff --git a/apps/authentication/serializers/connection_token.py b/apps/authentication/serializers/connection_token.py index 117ec2a04..767106aba 100644 --- a/apps/authentication/serializers/connection_token.py +++ b/apps/authentication/serializers/connection_token.py @@ -1,8 +1,8 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from assets.serializers import PlatformSerializer from assets.models import Asset, Domain, CommandFilterRule, Account, Platform +from assets.serializers import PlatformSerializer, AssetProtocolsSerializer from authentication.models import ConnectionToken from orgs.mixins.serializers import OrgResourceModelSerializerMixin from perms.serializers.permission import ActionChoicesField @@ -87,6 +87,7 @@ class ConnectionTokenUserSerializer(serializers.ModelSerializer): class ConnectionTokenAssetSerializer(serializers.ModelSerializer): """ Asset """ + protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols')) class Meta: model = Asset @@ -99,7 +100,7 @@ class ConnectionTokenAccountSerializer(serializers.ModelSerializer): class Meta: model = Account fields = [ - 'name', 'username', 'secret_type', 'secret', + 'name', 'username', 'secret_type', 'secret', ] From 4f718f9b1fa16c5221168a7237bf0c2254155989 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Mon, 28 Nov 2022 15:54:16 +0800 Subject: [PATCH 19/65] perf: account template secret api (#9127) Co-authored-by: feng <1304903146@qq.com> --- apps/assets/api/account/template.py | 21 +++++++++++++++++++-- apps/assets/serializers/account/template.py | 8 ++++++++ apps/assets/urls/api_urls.py | 1 + 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/apps/assets/api/account/template.py b/apps/assets/api/account/template.py index c04fd8ab6..dd9ee1d00 100644 --- a/apps/assets/api/account/template.py +++ b/apps/assets/api/account/template.py @@ -1,6 +1,10 @@ -from orgs.mixins.api import OrgBulkModelViewSet -from assets.models import AccountTemplate from assets import serializers +from assets.models import AccountTemplate +from rbac.permissions import RBACPermission +from authentication.const import ConfirmType +from common.mixins import RecordViewLogMixin +from common.permissions import UserConfirmation +from orgs.mixins.api import OrgBulkModelViewSet class AccountTemplateViewSet(OrgBulkModelViewSet): @@ -10,3 +14,16 @@ class AccountTemplateViewSet(OrgBulkModelViewSet): serializer_classes = { 'default': serializers.AccountTemplateSerializer } + + +class AccountTemplateSecretsViewSet(RecordViewLogMixin, AccountTemplateViewSet): + serializer_classes = { + 'default': serializers.AccountTemplateSecretSerializer, + } + http_method_names = ['get', 'options'] + # Todo: 记得打开 + # permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)] + rbac_perms = { + 'list': 'assets.view_accounttemplatesecret', + 'retrieve': 'assets.view_accounttemplatesecret', + } diff --git a/apps/assets/serializers/account/template.py b/apps/assets/serializers/account/template.py index 4599ca0e7..7a7de7f11 100644 --- a/apps/assets/serializers/account/template.py +++ b/apps/assets/serializers/account/template.py @@ -1,3 +1,4 @@ +from common.drf.serializers import SecretReadableMixin from assets.models import AccountTemplate from .base import BaseAccountSerializer @@ -17,3 +18,10 @@ class AccountTemplateSerializer(BaseAccountSerializer): # if not required_field_dict: # return # raise serializers.ValidationError(required_field_dict) + + +class AccountTemplateSecretSerializer(SecretReadableMixin, AccountTemplateSerializer): + class Meta(AccountTemplateSerializer.Meta): + extra_kwargs = { + 'secret': {'write_only': False}, + } diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index a7077aa39..773f5c348 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -16,6 +16,7 @@ router.register(r'webs', api.WebViewSet, 'web') router.register(r'clouds', api.CloudViewSet, 'cloud') router.register(r'accounts', api.AccountViewSet, 'account') router.register(r'account-templates', api.AccountTemplateViewSet, 'account-template') +router.register(r'account-template-secrets', api.AccountTemplateSecretsViewSet, 'account-template-secret') router.register(r'account-secrets', api.AccountSecretsViewSet, 'account-secret') router.register(r'platforms', api.AssetPlatformViewSet, 'platform') router.register(r'labels', api.LabelViewSet, 'label') From d0b9dd457f282ffabaaaf619869cf108e3b04dff Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Mon, 28 Nov 2022 16:12:06 +0800 Subject: [PATCH 20/65] perf: navigation date metrics --- apps/jumpserver/api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py index c7dbb6cd7..48b767299 100644 --- a/apps/jumpserver/api.py +++ b/apps/jumpserver/api.py @@ -31,7 +31,8 @@ class DatesLoginMetricMixin: query_params = self.request.query_params # monthly count = query_params.get('days') - return count if count else 0 + count = int(count) if count else 0 + return count @lazyproperty def sessions_queryset(self): From 742cac1e9001a9a363fd15661ac0dc963a8a2eef Mon Sep 17 00:00:00 2001 From: ibuler Date: Mon, 28 Nov 2022 17:57:33 +0800 Subject: [PATCH 21/65] =?UTF-8?q?pref:=20=E4=BF=AE=E6=94=B9=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E8=BF=9E=E6=8E=A5=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/terminal/const.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/apps/terminal/const.py b/apps/terminal/const.py index 3f653aaf7..21d79fd0a 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -56,11 +56,7 @@ class NativeClient(TextChoices): xshell = 'xshell', 'Xshell' # Magnus - mysql = 'mysql', 'mysql' - psql = 'psql', 'psql' - sqlplus = 'sqlplus', 'sqlplus' - redis = 'redis-cli', 'redis-cli' - mongodb = 'mongo', 'mongo' + db_client = 'db_client', _('DB Client') # Razor mstsc = 'mstsc', 'Remote Desktop' @@ -73,11 +69,11 @@ class NativeClient(TextChoices): 'windows': [cls.putty], }, Protocol.rdp: [cls.mstsc], - Protocol.mysql: [cls.mysql], - Protocol.oracle: [cls.sqlplus], - Protocol.postgresql: [cls.psql], - Protocol.redis: [cls.redis], - Protocol.mongodb: [cls.mongodb], + Protocol.mysql: [cls.db_client], + Protocol.oracle: [cls.db_client], + Protocol.postgresql: [cls.db_client], + Protocol.redis: [cls.db_client], + Protocol.mongodb: [cls.db_client], } return clients @@ -183,8 +179,8 @@ class TerminalType(TextChoices): cls.magnus: { 'listen': [], 'support': [ - Protocol.mysql, Protocol.postgresql, Protocol.oracle, - Protocol.mariadb + Protocol.mysql, Protocol.postgresql, + Protocol.oracle, Protocol.mariadb ], 'match': 'map' }, From 11636dafd8024a2684f7c905bff2f5a4f42c9a98 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Mon, 28 Nov 2022 18:43:58 +0800 Subject: [PATCH 22/65] perf: history account secret perm (#9128) Co-authored-by: feng <1304903146@qq.com> --- .../0113_alter_accounttemplate_options.py | 17 +++++++++++++ apps/assets/models/account.py | 4 ++++ .../migrations/0017_auto_20221128_1839.py | 24 +++++++++++++++++++ .../ops/migrations/0036_auto_20221128_1839.py | 21 ++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 apps/assets/migrations/0113_alter_accounttemplate_options.py create mode 100644 apps/authentication/migrations/0017_auto_20221128_1839.py create mode 100644 apps/ops/migrations/0036_auto_20221128_1839.py diff --git a/apps/assets/migrations/0113_alter_accounttemplate_options.py b/apps/assets/migrations/0113_alter_accounttemplate_options.py new file mode 100644 index 000000000..e635426c1 --- /dev/null +++ b/apps/assets/migrations/0113_alter_accounttemplate_options.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.14 on 2022-11-28 10:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0112_gateway_to_asset'), + ] + + operations = [ + migrations.AlterModelOptions( + name='accounttemplate', + options={'permissions': [('view_accounttemplatesecret', 'Can view asset account template secret'), ('change_accounttemplatesecret', 'Can change asset account template secret')], 'verbose_name': 'Account template'}, + ), + ] diff --git a/apps/assets/models/account.py b/apps/assets/models/account.py index cad5f9ded..930fd7882 100644 --- a/apps/assets/models/account.py +++ b/apps/assets/models/account.py @@ -94,6 +94,10 @@ class AccountTemplate(BaseAccount): unique_together = ( ('name', 'org_id'), ) + permissions = [ + ('view_accounttemplatesecret', _('Can view asset account template secret')), + ('change_accounttemplatesecret', _('Can change asset account template secret')), + ] def __str__(self): return self.username diff --git a/apps/authentication/migrations/0017_auto_20221128_1839.py b/apps/authentication/migrations/0017_auto_20221128_1839.py new file mode 100644 index 000000000..31a49267a --- /dev/null +++ b/apps/authentication/migrations/0017_auto_20221128_1839.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.14 on 2022-11-28 10:39 + +import common.db.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0016_auto_20221125_2240'), + ] + + operations = [ + migrations.AlterField( + model_name='connectiontoken', + name='input_secret', + field=common.db.fields.EncryptCharField(blank=True, default='', max_length=128, verbose_name='Input Secret'), + ), + migrations.AlterField( + model_name='connectiontoken', + name='input_username', + field=models.CharField(blank=True, default='', max_length=128, verbose_name='Input Username'), + ), + ] diff --git a/apps/ops/migrations/0036_auto_20221128_1839.py b/apps/ops/migrations/0036_auto_20221128_1839.py new file mode 100644 index 000000000..22bc435e2 --- /dev/null +++ b/apps/ops/migrations/0036_auto_20221128_1839.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.14 on 2022-11-28 10:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0035_jobexecution_org_id'), + ] + + operations = [ + migrations.AlterModelOptions( + name='job', + options={'ordering': ['date_created']}, + ), + migrations.AlterModelOptions( + name='jobexecution', + options={'ordering': ['-date_created']}, + ), + ] From 3052aa759c98e3a180bb9a022323d8b087817724 Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Mon, 28 Nov 2022 21:54:20 +0800 Subject: [PATCH 23/65] perf: ticket login asset acl --- apps/acls/api/login_asset_check.py | 4 ++-- apps/acls/models/login_asset_acl.py | 14 +++++++------- apps/acls/serializers/login_asset_check.py | 19 ++++++++++++------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/apps/acls/api/login_asset_check.py b/apps/acls/api/login_asset_check.py index bedf78d41..331c42768 100644 --- a/apps/acls/api/login_asset_check.py +++ b/apps/acls/api/login_asset_check.py @@ -26,7 +26,7 @@ class LoginAssetCheckAPI(CreateAPIView): def check_if_need_confirm(self): queries = { 'user': self.serializer.user, 'asset': self.serializer.asset, - 'account': self.serializer.account, + 'account_username': self.serializer.username, 'action': LoginAssetACL.ActionChoices.login_confirm } with tmp_to_org(self.serializer.org): @@ -45,7 +45,7 @@ class LoginAssetCheckAPI(CreateAPIView): ticket = LoginAssetACL.create_login_asset_confirm_ticket( user=self.serializer.user, asset=self.serializer.asset, - account=self.serializer.account, + account_username=self.serializer.username, assignees=acl.reviewers.all(), org_id=self.serializer.org.id, ) diff --git a/apps/acls/models/login_asset_acl.py b/apps/acls/models/login_asset_acl.py index 2ad9363e5..b01e4aed1 100644 --- a/apps/acls/models/login_asset_acl.py +++ b/apps/acls/models/login_asset_acl.py @@ -43,11 +43,11 @@ class LoginAssetACL(BaseACL, OrgModelMixin): return self.name @classmethod - def filter(cls, user, asset, account, action): + def filter(cls, user, asset, account_username, action): queryset = cls.objects.filter(action=action) queryset = cls.filter_user(user, queryset) queryset = cls.filter_asset(asset, queryset) - queryset = cls.filter_account(account, queryset) + queryset = cls.filter_account(account_username, queryset) return queryset @classmethod @@ -69,18 +69,18 @@ class LoginAssetACL(BaseACL, OrgModelMixin): return queryset @classmethod - def filter_account(cls, account, queryset): + def filter_account(cls, account_username, queryset): queryset = queryset.filter( - Q(accounts__name_group__contains=account.name) | + Q(accounts__name_group__contains=account_username) | Q(accounts__name_group__contains='*') ).filter( - Q(accounts__username_group__contains=account.username) | + Q(accounts__username_group__contains=account_username) | Q(accounts__username_group__contains='*') ) return queryset @classmethod - def create_login_asset_confirm_ticket(cls, user, asset, account, assignees, org_id): + def create_login_asset_confirm_ticket(cls, user, asset, account_username, assignees, org_id): from tickets.const import TicketType from tickets.models import ApplyLoginAssetTicket title = _('Login asset confirm') + ' ({})'.format(user) @@ -90,7 +90,7 @@ class LoginAssetACL(BaseACL, OrgModelMixin): 'applicant': user, 'apply_login_user': user, 'apply_login_asset': asset, - 'apply_login_account': str(account), + 'apply_login_account': account_username, 'type': TicketType.login_asset_confirm, } ticket = ApplyLoginAssetTicket.objects.create(**data) diff --git a/apps/acls/serializers/login_asset_check.py b/apps/acls/serializers/login_asset_check.py index 2240cb8d6..279feb3b6 100644 --- a/apps/acls/serializers/login_asset_check.py +++ b/apps/acls/serializers/login_asset_check.py @@ -10,15 +10,13 @@ __all__ = ['LoginAssetCheckSerializer'] class LoginAssetCheckSerializer(serializers.Serializer): user_id = serializers.UUIDField(required=True, allow_null=False) asset_id = serializers.UUIDField(required=True, allow_null=False) - account_id = serializers.UUIDField(required=True, allow_null=False) account_username = serializers.CharField(max_length=128, default='') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.user = None self.asset = None - self.account = None - self._account_username = None + self.username = None def validate_user_id(self, user_id): self.user = self.validate_object_exist(User, user_id) @@ -28,10 +26,6 @@ class LoginAssetCheckSerializer(serializers.Serializer): self.asset = self.validate_object_exist(Asset, asset_id) return asset_id - def validate_account_id(self, account_id): - self.account = self.validate_object_exist(Account, account_id) - return account_id - @staticmethod def validate_object_exist(model, field_id): with tmp_to_root_org(): @@ -41,6 +35,17 @@ class LoginAssetCheckSerializer(serializers.Serializer): raise serializers.ValidationError(error) return obj + def validate_account_username(self, account_username): + asset_id = self.initial_data.get('asset_id') + account = Account.objects.filter( + username=account_username, asset_id=asset_id + ).first() + if not account: + error = 'Account username does not exist' + raise serializers.ValidationError(error) + self.username = account_username + return account_username + @lazyproperty def org(self): return self.asset.org From f6bdc7f81cfe4fc12e706570b33849922529eb28 Mon Sep 17 00:00:00 2001 From: ibuler Date: Mon, 28 Nov 2022 22:58:43 +0800 Subject: [PATCH 24/65] =?UTF-8?q?pref:=20=E6=9A=82=E5=AD=98=20=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E8=BF=9E=E6=8E=A5=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/api/connection_token.py | 32 +++++++++++++------ .../migrations/0016_auto_20221125_2240.py | 12 +++---- .../migrations/0017_auto_20221128_2148.py | 20 ++++++++++++ .../authentication/models/connection_token.py | 1 + .../serializers/connection_token.py | 24 +++++++------- .../ops/migrations/0036_auto_20221128_2148.py | 21 ++++++++++++ apps/terminal/const.py | 24 ++++++++------ 7 files changed, 96 insertions(+), 38 deletions(-) create mode 100644 apps/authentication/migrations/0017_auto_20221128_2148.py create mode 100644 apps/ops/migrations/0036_auto_20221128_2148.py diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index e08ba5291..777c02e6a 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -11,6 +11,7 @@ from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied from rest_framework.request import Request from rest_framework.response import Response +from rest_framework.serializers import ValidationError from common.drf.api import JMSModelViewSet from common.http import is_true @@ -18,6 +19,7 @@ from common.utils import random_string from orgs.mixins.api import RootOrgViewMixin from perms.models import ActionChoices from terminal.models import EndpointRule +from terminal.const import NativeClient from ..models import ConnectionToken from ..serializers import ( ConnectionTokenSerializer, ConnectionTokenSecretSerializer, @@ -128,19 +130,20 @@ class RDPFileClientProtocolURLMixin: return true_value if is_true(os.getenv(env_key, env_default)) else false_value def get_client_protocol_data(self, token: ConnectionToken): - protocol = token.protocol username = token.user.username rdp_config = ssh_token = '' - if protocol == 'rdp': - filename, rdp_config = self.get_rdp_file_info(token) - elif protocol == 'ssh': + connect_method = token.connect_method + + if connect_method == NativeClient.ssh: filename, ssh_token = self.get_ssh_token(token) + elif connect_method == NativeClient.mstsc: + filename, rdp_config = self.get_rdp_file_info(token) else: - raise ValueError('Protocol not support: {}'.format(protocol)) + raise ValueError('Protocol not support: {}'.format(connect_method)) return { "filename": filename, - "protocol": protocol, + "protocol": token.protocol, "username": username, "token": ssh_token, "config": rdp_config @@ -234,14 +237,25 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView rbac_perm = 'authentication.view_connectiontokensecret' if not request.user.has_perm(rbac_perm): raise PermissionDenied('Not allow to view secret') - token_id = request.data.get('token') or '' + + token_id = request.data.get('id') or '' token = get_object_or_404(ConnectionToken, pk=token_id) + if token.is_expired: + raise ValidationError({'id': 'Token is expired'}) + token.is_valid() serializer = self.get_serializer(instance=token) + expire_now = request.data.get('expire_now', True) + if expire_now: + token.expire() + return Response(serializer.data, status=status.HTTP_200_OK) def get_queryset(self): - return ConnectionToken.objects.filter(user=self.request.user) + queryset = ConnectionToken.objects\ + .filter(user=self.request.user)\ + .filter(date_expired__lt=timezone.now()) + return queryset def get_user(self, serializer): return self.request.user @@ -299,7 +313,7 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet): def renewal(self, request, *args, **kwargs): from common.utils.timezone import as_current_tz - token_id = request.data.get('token') or '' + token_id = request.data.get('id') or '' token = get_object_or_404(ConnectionToken, pk=token_id) date_expired = as_current_tz(token.date_expired) if token.is_expired: diff --git a/apps/authentication/migrations/0016_auto_20221125_2240.py b/apps/authentication/migrations/0016_auto_20221125_2240.py index 92478a523..ac1294f86 100644 --- a/apps/authentication/migrations/0016_auto_20221125_2240.py +++ b/apps/authentication/migrations/0016_auto_20221125_2240.py @@ -26,20 +26,20 @@ class Migration(migrations.Migration): old_name='username', new_name='input_username', ), - migrations.AddField( - model_name='connectiontoken', - name='input_secret', - field=common.db.fields.EncryptCharField(default='', max_length=128, verbose_name='Input Secret'), - ), migrations.AlterField( model_name='connectiontoken', name='account_name', field=models.CharField(max_length=128, verbose_name='Account name'), ), + migrations.AlterField( + model_name='connectiontoken', + name='input_secret', + field=common.db.fields.EncryptCharField(blank=True, default='', max_length=128, verbose_name='Input Secret'), + ), migrations.AlterField( model_name='connectiontoken', name='input_username', - field=models.CharField(default='', max_length=128, verbose_name='Input Username'), + field=models.CharField(blank=True, default='', max_length=128, verbose_name='Input Username'), ), migrations.AlterField( model_name='connectiontoken', diff --git a/apps/authentication/migrations/0017_auto_20221128_2148.py b/apps/authentication/migrations/0017_auto_20221128_2148.py new file mode 100644 index 000000000..396089bbc --- /dev/null +++ b/apps/authentication/migrations/0017_auto_20221128_2148.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.14 on 2022-11-28 13:48 + +import common.db.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0016_auto_20221125_2240'), + ] + + operations = [ + migrations.AddField( + model_name='connectiontoken', + name='connect_method', + field=models.CharField(default='web_ui', max_length=32, verbose_name='Connect method'), + preserve_default=False, + ), + ] diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py index 82ad3589f..5505f81a3 100644 --- a/apps/authentication/models/connection_token.py +++ b/apps/authentication/models/connection_token.py @@ -34,6 +34,7 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel): protocol = models.CharField( choices=Protocol.choices, max_length=16, default=Protocol.ssh, verbose_name=_("Protocol") ) + connect_method = models.CharField(max_length=32, verbose_name=_("Connect method")) user_display = models.CharField(max_length=128, default='', verbose_name=_("User display")) asset_display = models.CharField(max_length=128, default='', verbose_name=_("Asset display")) date_expired = models.DateTimeField( diff --git a/apps/authentication/serializers/connection_token.py b/apps/authentication/serializers/connection_token.py index 767106aba..0a223306a 100644 --- a/apps/authentication/serializers/connection_token.py +++ b/apps/authentication/serializers/connection_token.py @@ -109,16 +109,10 @@ class ConnectionTokenGatewaySerializer(serializers.ModelSerializer): class Meta: model = Asset - fields = ['id', 'address', 'port', 'username', 'password', 'private_key'] - - -class ConnectionTokenDomainSerializer(serializers.ModelSerializer): - """ Domain """ - gateways = ConnectionTokenGatewaySerializer(many=True, read_only=True) - - class Meta: - model = Domain - fields = ['id', 'name', 'gateways'] + fields = [ + 'id', 'address', 'port', 'username', + 'password', 'private_key' + ] class ConnectionTokenCmdFilterRuleSerializer(serializers.ModelSerializer): @@ -143,6 +137,7 @@ class ConnectionTokenPlatform(PlatformSerializer): class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin): + expire_now = serializers.BooleanField(label=_('Expired now'), default=True) user = ConnectionTokenUserSerializer(read_only=True) asset = ConnectionTokenAssetSerializer(read_only=True) platform = ConnectionTokenPlatform(read_only=True) @@ -155,7 +150,10 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin): class Meta: model = ConnectionToken fields = [ - 'id', 'value', 'user', 'asset', 'account', - 'protocol', 'domain', 'gateway', - 'actions', 'expire_at', 'platform', + 'id', 'value', 'user', 'asset', 'platform', 'account', + 'protocol', 'gateway', 'actions', 'expire_at', 'expire_now', ] + extra_kwargs = { + 'value': {'read_only': True}, + 'expire_now': {'write_only': True}, + } diff --git a/apps/ops/migrations/0036_auto_20221128_2148.py b/apps/ops/migrations/0036_auto_20221128_2148.py new file mode 100644 index 000000000..b2da1a35a --- /dev/null +++ b/apps/ops/migrations/0036_auto_20221128_2148.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.14 on 2022-11-28 13:48 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0035_jobexecution_org_id'), + ] + + operations = [ + migrations.AlterModelOptions( + name='job', + options={'ordering': ['date_created']}, + ), + migrations.AlterModelOptions( + name='jobexecution', + options={'ordering': ['-date_created']}, + ), + ] diff --git a/apps/terminal/const.py b/apps/terminal/const.py index 21d79fd0a..177d50e20 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -96,17 +96,21 @@ class NativeClient(TextChoices): @classmethod def get_launch_command(cls, name, os='windows'): commands = { - 'ssh': 'ssh {username}@{hostname} -p {port}', - 'putty': 'putty -ssh {username}@{hostname} -P {port}', - 'xshell': '-url ssh://root:passwd@192.168.10.100', - 'mysql': 'mysql -h {hostname} -P {port} -u {username} -p', - 'psql': { - 'default': 'psql -h {hostname} -p {port} -U {username} -W', - 'windows': 'psql /h {hostname} /p {port} /U {username} -W', + cls.ssh: 'ssh {token.id}@{endpoint.ip} -p {endpoint.port}', + cls.putty: 'putty-ssh {token.id}@{endpoint.ip} -P {endpoint.port}', + cls.xshell: 'xshell -url ssh://{token.id}:{token.value}@{endpoint.ip}:{endpoint.port}', + # 'mysql': 'mysql -h {hostname} -P {port} -u {username} -p', + # 'psql': { + # 'default': 'psql -h {hostname} -p {port} -U {username} -W', + # 'windows': 'psql /h {hostname} /p {port} /U {username} -W', + # }, + # 'sqlplus': 'sqlplus {username}/{password}@{hostname}:{port}', + # 'redis': 'redis-cli -h {hostname} -p {port} -a {password}', + cls.mstsc: { + 'command': "$open_file$", + 'file': { + } }, - 'sqlplus': 'sqlplus {username}/{password}@{hostname}:{port}', - 'redis': 'redis-cli -h {hostname} -p {port} -a {password}', - 'mstsc': 'mstsc /v:{hostname}:{port}', } command = commands.get(name) if isinstance(command, dict): From e4edf3be02a729b6e8b5c7ca394d3a80e817c678 Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Tue, 29 Nov 2022 09:34:43 +0800 Subject: [PATCH 25/65] perf: migrate --- .../migrations/0016_auto_20221125_2240.py | 10 --------- .../migrations/0017_auto_20221128_1839.py | 11 ++++++++-- .../migrations/0017_auto_20221128_2148.py | 20 ------------------ .../ops/migrations/0036_auto_20221128_2148.py | 21 ------------------- 4 files changed, 9 insertions(+), 53 deletions(-) delete mode 100644 apps/authentication/migrations/0017_auto_20221128_2148.py delete mode 100644 apps/ops/migrations/0036_auto_20221128_2148.py diff --git a/apps/authentication/migrations/0016_auto_20221125_2240.py b/apps/authentication/migrations/0016_auto_20221125_2240.py index ac1294f86..745fcef2b 100644 --- a/apps/authentication/migrations/0016_auto_20221125_2240.py +++ b/apps/authentication/migrations/0016_auto_20221125_2240.py @@ -31,16 +31,6 @@ class Migration(migrations.Migration): name='account_name', field=models.CharField(max_length=128, verbose_name='Account name'), ), - migrations.AlterField( - model_name='connectiontoken', - name='input_secret', - field=common.db.fields.EncryptCharField(blank=True, default='', max_length=128, verbose_name='Input Secret'), - ), - migrations.AlterField( - model_name='connectiontoken', - name='input_username', - field=models.CharField(blank=True, default='', max_length=128, verbose_name='Input Username'), - ), migrations.AlterField( model_name='connectiontoken', name='value', diff --git a/apps/authentication/migrations/0017_auto_20221128_1839.py b/apps/authentication/migrations/0017_auto_20221128_1839.py index 31a49267a..8c392cb92 100644 --- a/apps/authentication/migrations/0017_auto_20221128_1839.py +++ b/apps/authentication/migrations/0017_auto_20221128_1839.py @@ -11,10 +11,17 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AlterField( + migrations.AddField( + model_name='connectiontoken', + name='connect_method', + field=models.CharField(default='web_ui', max_length=32, verbose_name='Connect method'), + preserve_default=False, + ), + migrations.AddField( model_name='connectiontoken', name='input_secret', - field=common.db.fields.EncryptCharField(blank=True, default='', max_length=128, verbose_name='Input Secret'), + field=common.db.fields.EncryptCharField(blank=True, default='', max_length=128, + verbose_name='Input Secret'), ), migrations.AlterField( model_name='connectiontoken', diff --git a/apps/authentication/migrations/0017_auto_20221128_2148.py b/apps/authentication/migrations/0017_auto_20221128_2148.py deleted file mode 100644 index 396089bbc..000000000 --- a/apps/authentication/migrations/0017_auto_20221128_2148.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 3.2.14 on 2022-11-28 13:48 - -import common.db.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('authentication', '0016_auto_20221125_2240'), - ] - - operations = [ - migrations.AddField( - model_name='connectiontoken', - name='connect_method', - field=models.CharField(default='web_ui', max_length=32, verbose_name='Connect method'), - preserve_default=False, - ), - ] diff --git a/apps/ops/migrations/0036_auto_20221128_2148.py b/apps/ops/migrations/0036_auto_20221128_2148.py deleted file mode 100644 index b2da1a35a..000000000 --- a/apps/ops/migrations/0036_auto_20221128_2148.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.2.14 on 2022-11-28 13:48 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('ops', '0035_jobexecution_org_id'), - ] - - operations = [ - migrations.AlterModelOptions( - name='job', - options={'ordering': ['date_created']}, - ), - migrations.AlterModelOptions( - name='jobexecution', - options={'ordering': ['-date_created']}, - ), - ] From 0981cd1ed1d80aa79c8afa4f36b94a1a53143303 Mon Sep 17 00:00:00 2001 From: ibuler Date: Tue, 29 Nov 2022 14:42:04 +0800 Subject: [PATCH 26/65] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=20Connect=20to?= =?UTF-8?q?ken=20=E6=95=B0=E6=8D=AE=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/api/connection_token.py | 64 ++++++++----------- .../migrations/0016_auto_20221125_2240.py | 15 ++++- .../migrations/0017_auto_20221128_1839.py | 13 ---- .../0018_connectiontoken_endpoint_protocol.py | 19 ++++++ .../authentication/models/connection_token.py | 3 + .../serializers/connection_token.py | 10 ++- apps/common/utils/django.py | 19 +++++- apps/common/utils/http.py | 7 +- apps/terminal/api/component/terminal.py | 9 +-- apps/terminal/const.py | 55 ++++++++++------ apps/terminal/serializers/terminal.py | 4 +- 11 files changed, 129 insertions(+), 89 deletions(-) create mode 100644 apps/authentication/migrations/0018_connectiontoken_endpoint_protocol.py diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 777c02e6a..b8f461d52 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -16,10 +16,11 @@ from rest_framework.serializers import ValidationError from common.drf.api import JMSModelViewSet from common.http import is_true from common.utils import random_string +from common.utils.django import get_request_os from orgs.mixins.api import RootOrgViewMixin from perms.models import ActionChoices -from terminal.models import EndpointRule from terminal.const import NativeClient +from terminal.models import EndpointRule from ..models import ConnectionToken from ..serializers import ( ConnectionTokenSerializer, ConnectionTokenSecretSerializer, @@ -130,42 +131,32 @@ class RDPFileClientProtocolURLMixin: return true_value if is_true(os.getenv(env_key, env_default)) else false_value def get_client_protocol_data(self, token: ConnectionToken): - username = token.user.username - rdp_config = ssh_token = '' - connect_method = token.connect_method + _os = get_request_os(self.request) - if connect_method == NativeClient.ssh: - filename, ssh_token = self.get_ssh_token(token) - elif connect_method == NativeClient.mstsc: - filename, rdp_config = self.get_rdp_file_info(token) - else: - raise ValueError('Protocol not support: {}'.format(connect_method)) + connect_method = getattr(NativeClient, token.connect_method, None) + if connect_method is None: + raise ValueError('Connect method not support: {}'.format(token.connect_method)) - return { - "filename": filename, - "protocol": token.protocol, - "username": username, - "token": ssh_token, - "config": rdp_config - } - - def get_ssh_token(self, token: ConnectionToken): - if token.asset: - name = token.asset.name - else: - name = '*' - prefix_name = f'{token.user.username}-{name}' - filename = self.get_connect_filename(prefix_name) - - endpoint = self.get_smart_endpoint(protocol='ssh', asset=token.asset) data = { - 'ip': endpoint.host, - 'port': str(endpoint.ssh_port), - 'username': 'JMS-{}'.format(str(token.id)), - 'password': token.value + 'id': str(token.id), + 'value': token.value, + 'cmd': '', + 'file': {} } - token = json.dumps(data) - return filename, token + + if connect_method == NativeClient.mstsc: + filename, content = self.get_rdp_file_info(token) + data.update({ + 'file': { + 'filename': filename, + 'content': content, + } + }) + else: + endpoint = self.get_smart_endpoint(protocol=token.endpoint_protocol, asset=token.asset) + cmd = NativeClient.get_launch_command(connect_method, token, endpoint) + data.update({'cmd': cmd}) + return data def get_smart_endpoint(self, protocol, asset=None): target_ip = asset.get_target_ip() if asset else '' @@ -223,6 +214,7 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView 'get_secret_detail': ConnectionTokenSecretSerializer, } rbac_perms = { + 'list': 'authentication.view_connectiontoken', 'retrieve': 'authentication.view_connectiontoken', 'create': 'authentication.add_connectiontoken', 'expire': 'authentication.add_connectiontoken', @@ -252,9 +244,9 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView return Response(serializer.data, status=status.HTTP_200_OK) def get_queryset(self): - queryset = ConnectionToken.objects\ - .filter(user=self.request.user)\ - .filter(date_expired__lt=timezone.now()) + queryset = ConnectionToken.objects \ + .filter(user=self.request.user) \ + .filter(date_expired__gt=timezone.now()) return queryset def get_user(self, serializer): diff --git a/apps/authentication/migrations/0016_auto_20221125_2240.py b/apps/authentication/migrations/0016_auto_20221125_2240.py index 745fcef2b..041a29fc6 100644 --- a/apps/authentication/migrations/0016_auto_20221125_2240.py +++ b/apps/authentication/migrations/0016_auto_20221125_2240.py @@ -1,11 +1,11 @@ # Generated by Django 3.2.14 on 2022-11-25 14:40 -import common.db.fields from django.db import migrations, models +import common.db.fields + class Migration(migrations.Migration): - dependencies = [ ('authentication', '0015_alter_connectiontoken_login'), ] @@ -36,4 +36,15 @@ class Migration(migrations.Migration): name='value', field=models.CharField(default='', max_length=64, verbose_name='Value'), ), + migrations.AddField( + model_name='connectiontoken', + name='input_secret', + field=common.db.fields.EncryptCharField(blank=True, default='', max_length=128, + verbose_name='Input Secret'), + ), + migrations.AlterField( + model_name='connectiontoken', + name='input_username', + field=models.CharField(blank=True, default='', max_length=128, verbose_name='Input Username'), + ), ] diff --git a/apps/authentication/migrations/0017_auto_20221128_1839.py b/apps/authentication/migrations/0017_auto_20221128_1839.py index 8c392cb92..bcdb71020 100644 --- a/apps/authentication/migrations/0017_auto_20221128_1839.py +++ b/apps/authentication/migrations/0017_auto_20221128_1839.py @@ -1,11 +1,9 @@ # Generated by Django 3.2.14 on 2022-11-28 10:39 -import common.db.fields from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ('authentication', '0016_auto_20221125_2240'), ] @@ -17,15 +15,4 @@ class Migration(migrations.Migration): field=models.CharField(default='web_ui', max_length=32, verbose_name='Connect method'), preserve_default=False, ), - migrations.AddField( - model_name='connectiontoken', - name='input_secret', - field=common.db.fields.EncryptCharField(blank=True, default='', max_length=128, - verbose_name='Input Secret'), - ), - migrations.AlterField( - model_name='connectiontoken', - name='input_username', - field=models.CharField(blank=True, default='', max_length=128, verbose_name='Input Username'), - ), ] diff --git a/apps/authentication/migrations/0018_connectiontoken_endpoint_protocol.py b/apps/authentication/migrations/0018_connectiontoken_endpoint_protocol.py new file mode 100644 index 000000000..f267a62fd --- /dev/null +++ b/apps/authentication/migrations/0018_connectiontoken_endpoint_protocol.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.14 on 2022-11-29 04:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0017_auto_20221128_1839'), + ] + + operations = [ + migrations.AddField( + model_name='connectiontoken', + name='endpoint_protocol', + field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('mariadb', 'MariaDB'), ('oracle', 'Oracle'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'), ('mongodb', 'MongoDB'), ('k8s', 'K8S'), ('http', 'HTTP'), ('None', ' Settings')], default='', max_length=16, verbose_name='Endpoint protocol'), + preserve_default=False, + ), + ] diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py index 5505f81a3..7f4e7f42b 100644 --- a/apps/authentication/models/connection_token.py +++ b/apps/authentication/models/connection_token.py @@ -35,6 +35,9 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel): choices=Protocol.choices, max_length=16, default=Protocol.ssh, verbose_name=_("Protocol") ) connect_method = models.CharField(max_length=32, verbose_name=_("Connect method")) + endpoint_protocol = models.CharField( + choices=Protocol.choices, max_length=16, verbose_name=_("Endpoint protocol") + ) user_display = models.CharField(max_length=128, default='', verbose_name=_("User display")) asset_display = models.CharField(max_length=128, default='', verbose_name=_("Asset display")) date_expired = models.DateTimeField( diff --git a/apps/authentication/serializers/connection_token.py b/apps/authentication/serializers/connection_token.py index 0a223306a..5915f542b 100644 --- a/apps/authentication/serializers/connection_token.py +++ b/apps/authentication/serializers/connection_token.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from assets.models import Asset, Domain, CommandFilterRule, Account, Platform +from assets.models import Asset, CommandFilterRule, Account, Platform from assets.serializers import PlatformSerializer, AssetProtocolsSerializer from authentication.models import ConnectionToken from orgs.mixins.serializers import OrgResourceModelSerializerMixin @@ -21,21 +21,19 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): model = ConnectionToken fields_mini = ['id', 'value'] fields_small = fields_mini + [ - 'protocol', 'account_name', + 'user', 'asset', 'account_name', 'input_username', 'input_secret', + 'connect_method', 'endpoint_protocol', 'protocol', 'actions', 'date_expired', 'date_created', 'date_updated', 'created_by', 'updated_by', 'org_id', 'org_name', ] - fields_fk = [ - 'user', 'asset', - ] read_only_fields = [ # 普通 Token 不支持指定 user 'user', 'expire_time', 'user_display', 'asset_display', ] - fields = fields_small + fields_fk + read_only_fields + fields = fields_small + read_only_fields extra_kwargs = { 'value': {'read_only': True}, } diff --git a/apps/common/utils/django.py b/apps/common/utils/django.py index 1f3f83282..2c4808d16 100644 --- a/apps/common/utils/django.py +++ b/apps/common/utils/django.py @@ -2,11 +2,11 @@ # import re -from django.shortcuts import reverse as dj_reverse from django.conf import settings -from django.utils import timezone from django.db import models from django.db.models.signals import post_save, pre_save +from django.shortcuts import reverse as dj_reverse +from django.utils import timezone UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}') @@ -80,3 +80,18 @@ def bulk_create_with_signal(cls: models.Model, items, **kwargs): for i in items: post_save.send(sender=cls, instance=i, created=True) return result + + +def get_request_os(request): + """获取请求的操作系统""" + agent = request.META.get('HTTP_USER_AGENT', '').lower() + + if agent is None: + return 'unknown' + if 'windows' in agent.lower(): + return 'windows' + if 'mac' in agent.lower(): + return 'mac' + if 'linux' in agent.lower(): + return 'linux' + return 'unknown' diff --git a/apps/common/utils/http.py b/apps/common/utils/http.py index 185397881..ab39c4fa2 100644 --- a/apps/common/utils/http.py +++ b/apps/common/utils/http.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- # -import time -from email.utils import formatdate import calendar import threading +import time +from email.utils import formatdate _STRPTIME_LOCK = threading.Lock() @@ -35,3 +35,6 @@ def http_to_unixtime(time_string): def iso8601_to_unixtime(time_string): """把ISO8601时间字符串(形如,2012-02-24T06:07:48.000Z)转换为UNIX时间,精确到秒。""" return to_unixtime(time_string, _ISO8601_FORMAT) + + + diff --git a/apps/terminal/api/component/terminal.py b/apps/terminal/api/component/terminal.py index e3aa4afb3..df14296f5 100644 --- a/apps/terminal/api/component/terminal.py +++ b/apps/terminal/api/component/terminal.py @@ -12,6 +12,7 @@ from common.drf.api import JMSBulkModelViewSet from common.exceptions import JMSException from common.permissions import IsValidUser from common.permissions import WithBootstrapToken +from common.utils import get_request_os from terminal import serializers from terminal.const import TerminalType from terminal.models import Terminal @@ -77,13 +78,7 @@ class ConnectMethodListApi(generics.ListAPIView): permission_classes = [IsValidUser] def get_queryset(self): - user_agent = self.request.META['HTTP_USER_AGENT'].lower() - if 'macintosh' in user_agent: - os = 'macos' - elif 'windows' in user_agent: - os = 'windows' - else: - os = 'linux' + os = get_request_os(self.request) return TerminalType.get_protocols_connect_methods(os) def list(self, request, *args, **kwargs): diff --git a/apps/terminal/const.py b/apps/terminal/const.py index 177d50e20..40c89ff24 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -56,7 +56,11 @@ class NativeClient(TextChoices): xshell = 'xshell', 'Xshell' # Magnus - db_client = 'db_client', _('DB Client') + mysql = 'db_client_mysql', _('DB Client') + psql = 'db_client_psql', _('DB Client') + sqlplus = 'db_client_sqlplus', _('DB Client') + redis = 'db_client_redis', _('DB Client') + mongodb = 'db_client_mongodb', _('DB Client') # Razor mstsc = 'mstsc', 'Remote Desktop' @@ -69,14 +73,23 @@ class NativeClient(TextChoices): 'windows': [cls.putty], }, Protocol.rdp: [cls.mstsc], - Protocol.mysql: [cls.db_client], - Protocol.oracle: [cls.db_client], - Protocol.postgresql: [cls.db_client], - Protocol.redis: [cls.db_client], - Protocol.mongodb: [cls.db_client], + Protocol.mysql: [cls.mysql], + Protocol.oracle: [cls.sqlplus], + Protocol.postgresql: [cls.psql], + Protocol.redis: [cls.redis], + Protocol.mongodb: [cls.mongodb], } return clients + @classmethod + def get_target_protocol(cls, name, os): + for protocol, clients in cls.get_native_clients().items(): + if isinstance(clients, dict): + clients = clients.get(os) or clients.get('default') + if name in clients: + return protocol + return None + @classmethod def get_methods(cls, os='windows'): clients_map = cls.get_native_clients() @@ -94,23 +107,18 @@ class NativeClient(TextChoices): return methods @classmethod - def get_launch_command(cls, name, os='windows'): + def get_launch_command(cls, name, token, endpoint, os='windows'): commands = { - cls.ssh: 'ssh {token.id}@{endpoint.ip} -p {endpoint.port}', - cls.putty: 'putty-ssh {token.id}@{endpoint.ip} -P {endpoint.port}', - cls.xshell: 'xshell -url ssh://{token.id}:{token.value}@{endpoint.ip}:{endpoint.port}', - # 'mysql': 'mysql -h {hostname} -P {port} -u {username} -p', - # 'psql': { + cls.ssh: f'ssh {token.id}@{endpoint.host} -p {endpoint.ssh_port}', + cls.putty: f'putty -ssh {token.id}@{endpoint.host} -P {endpoint.ssh_port}', + cls.xshell: f'xshell -url ssh://{token.id}:{token.value}@{endpoint.host}:{endpoint.ssh_port}', + # cls.mysql: 'mysql -h {hostname} -P {port} -u {username} -p', + # cls.psql: { # 'default': 'psql -h {hostname} -p {port} -U {username} -W', # 'windows': 'psql /h {hostname} /p {port} /U {username} -W', # }, - # 'sqlplus': 'sqlplus {username}/{password}@{hostname}:{port}', - # 'redis': 'redis-cli -h {hostname} -p {port} -a {password}', - cls.mstsc: { - 'command': "$open_file$", - 'file': { - } - }, + # cls.sqlplus: 'sqlplus {username}/{password}@{hostname}:{port}', + # cls.redis: 'redis-cli -h {hostname} -p {port} -a {password}', } command = commands.get(name) if isinstance(command, dict): @@ -217,19 +225,26 @@ class TerminalType(TextChoices): methods[protocol.value].append({ 'value': web_protocol.value, 'label': web_protocol.label, + 'endpoint_protocol': 'http', 'type': 'web', 'component': component.value, }) # Native method methods[protocol.value].extend([ - {'component': component.value, 'type': 'native', **method} + { + 'component': component.value, + 'type': 'native', + 'endpoint_protocol': listen_protocol, + **method + } for method in native_methods[listen_protocol] ]) for protocol, applet_methods in applet_methods.items(): for method in applet_methods: method['type'] = 'applet' + method['listen'] = 'rdp' method['component'] = cls.tinker.value methods[protocol].extend(applet_methods) return methods diff --git a/apps/terminal/serializers/terminal.py b/apps/terminal/serializers/terminal.py index df32d89c2..f7f935f50 100644 --- a/apps/terminal/serializers/terminal.py +++ b/apps/terminal/serializers/terminal.py @@ -138,4 +138,6 @@ class TerminalRegistrationSerializer(serializers.ModelSerializer): class ConnectMethodSerializer(serializers.Serializer): value = serializers.CharField(max_length=128) label = serializers.CharField(max_length=128) - group = serializers.CharField(max_length=128) + type = serializers.CharField(max_length=128) + listen = serializers.CharField(max_length=128) + component = serializers.CharField(max_length=128) From 9412c5d42d481a4fe5e5bc8be209df2728152fba Mon Sep 17 00:00:00 2001 From: ibuler Date: Tue, 29 Nov 2022 14:45:29 +0800 Subject: [PATCH 27/65] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=20connect=20to?= =?UTF-8?q?ken=20=E6=95=B0=E6=8D=AE=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/api/connection_token.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index b8f461d52..3639206d3 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -140,7 +140,7 @@ class RDPFileClientProtocolURLMixin: data = { 'id': str(token.id), 'value': token.value, - 'cmd': '', + 'command': '', 'file': {} } @@ -148,14 +148,14 @@ class RDPFileClientProtocolURLMixin: filename, content = self.get_rdp_file_info(token) data.update({ 'file': { - 'filename': filename, + 'name': filename, 'content': content, } }) else: endpoint = self.get_smart_endpoint(protocol=token.endpoint_protocol, asset=token.asset) cmd = NativeClient.get_launch_command(connect_method, token, endpoint) - data.update({'cmd': cmd}) + data.update({'command': cmd}) return data def get_smart_endpoint(self, protocol, asset=None): From 3ac952f7355c1e2320a04cc90d3af66adabc3e56 Mon Sep 17 00:00:00 2001 From: ibuler Date: Tue, 29 Nov 2022 15:47:35 +0800 Subject: [PATCH 28/65] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=20connect=20to?= =?UTF-8?q?ken=20=E6=8B=89=E8=B5=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/api/connection_token.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 3639206d3..387081cc6 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -24,8 +24,7 @@ from terminal.models import EndpointRule from ..models import ConnectionToken from ..serializers import ( ConnectionTokenSerializer, ConnectionTokenSecretSerializer, - SuperConnectionTokenSerializer, ConnectionTokenDisplaySerializer, -) + SuperConnectionTokenSerializer, ) __all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet'] @@ -209,8 +208,6 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView search_fields = filterset_fields serializer_classes = { 'default': ConnectionTokenSerializer, - 'list': ConnectionTokenDisplaySerializer, - 'retrieve': ConnectionTokenDisplaySerializer, 'get_secret_detail': ConnectionTokenSecretSerializer, } rbac_perms = { From d849fd52bd7102aebc87bebce656c5fc7212d86a Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Tue, 29 Nov 2022 17:01:03 +0800 Subject: [PATCH 29/65] perf: domian add node (#9130) Co-authored-by: feng <1304903146@qq.com> --- apps/assets/migrations/0114_node_domain.py | 21 +++++++++++++++++++++ apps/assets/models/asset/common.py | 8 ++++++++ apps/assets/models/node.py | 9 +++++++-- apps/assets/serializers/domain.py | 17 ++++++++++++----- apps/ops/ansible/inventory.py | 7 +++++-- 5 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 apps/assets/migrations/0114_node_domain.py diff --git a/apps/assets/migrations/0114_node_domain.py b/apps/assets/migrations/0114_node_domain.py new file mode 100644 index 000000000..896a611d5 --- /dev/null +++ b/apps/assets/migrations/0114_node_domain.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.14 on 2022-11-29 05:14 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ('assets', '0113_alter_accounttemplate_options'), + ] + + operations = [ + migrations.AddField( + model_name='node', + name='domain', + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, + related_name='nodes', to='assets.domain', verbose_name='Domain' + ), + ), + ] diff --git a/apps/assets/models/asset/common.py b/apps/assets/models/asset/common.py index 81693252d..7f01c222d 100644 --- a/apps/assets/models/asset/common.py +++ b/apps/assets/models/asset/common.py @@ -105,6 +105,14 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel): def __str__(self): return '{0.name}({0.address})'.format(self) + def get_domains(self): + from ..domain import Domain + node_ids = self.get_all_nodes(flat=True) + domains = Domain.objects.filter( + Q(nodes__id__in=node_ids) | Q(id=self.domain_id) + ).distinct() + return domains + @property def specific(self): if not hasattr(self, self.category): diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index 0e98bce14..bf394e982 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -554,9 +554,14 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin): full_value = models.CharField(max_length=4096, verbose_name=_('Full value'), default='') child_mark = models.IntegerField(default=0) date_create = models.DateTimeField(auto_now_add=True) - parent_key = models.CharField(max_length=64, verbose_name=_("Parent key"), - db_index=True, default='') + parent_key = models.CharField( + max_length=64, verbose_name=_("Parent key"), db_index=True, default='' + ) assets_amount = models.IntegerField(default=0) + domain = models.ForeignKey( + "assets.Domain", null=True, blank=True, related_name='nodes', + verbose_name=_("Domain"), on_delete=models.SET_NULL + ) objects = OrgManager.from_queryset(NodeQuerySet)() is_node = True diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py index 9e495d104..617aa34bc 100644 --- a/apps/assets/serializers/domain.py +++ b/apps/assets/serializers/domain.py @@ -4,19 +4,22 @@ from rest_framework import serializers from rest_framework.generics import get_object_or_404 from django.utils.translation import ugettext_lazy as _ -from orgs.mixins.serializers import BulkOrgResourceModelSerializer, OrgResourceSerializerMixin +from orgs.mixins.serializers import BulkOrgResourceModelSerializer from common.drf.serializers import SecretReadableMixin, WritableNestedModelSerializer from common.drf.fields import ObjectRelatedField, EncryptedField -from assets.models import Platform, Node from assets.const import SecretType, GATEWAY_NAME from ..serializers import AssetProtocolsSerializer -from ..models import Domain, Asset, Account, Host +from ..models import Platform, Domain, Node, Asset, Account, Host from .utils import validate_password_for_ansible, validate_ssh_key class DomainSerializer(BulkOrgResourceModelSerializer): + node_count = serializers.SerializerMethodField(label=_('Nodes amount')) asset_count = serializers.SerializerMethodField(label=_('Assets amount')) gateway_count = serializers.SerializerMethodField(label=_('Gateways count')) + nodes = ObjectRelatedField( + many=True, required=False, queryset=Node.objects, label=_('Node') + ) assets = ObjectRelatedField( many=True, required=False, queryset=Asset.objects, label=_('Asset') ) @@ -25,14 +28,18 @@ class DomainSerializer(BulkOrgResourceModelSerializer): model = Domain fields_mini = ['id', 'name'] fields_small = fields_mini + ['comment'] - fields_m2m = ['assets'] - read_only_fields = ['asset_count', 'gateway_count', 'date_created'] + fields_m2m = ['nodes', 'assets'] + read_only_fields = ['node_count', 'asset_count', 'gateway_count', 'date_created'] fields = fields_small + fields_m2m + read_only_fields extra_kwargs = { 'assets': {'required': False, 'label': _('Assets')}, } + @staticmethod + def get_node_count(obj): + return obj.nodes.count() + @staticmethod def get_asset_count(obj): return obj.assets.count() diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py index 1e006ae77..aa7f8186d 100644 --- a/apps/ops/ansible/inventory.py +++ b/apps/ops/ansible/inventory.py @@ -1,6 +1,7 @@ # ~*~ coding: utf-8 ~*~ import json import os +import random from collections import defaultdict from django.utils.translation import gettext as _ @@ -117,8 +118,10 @@ class JMSInventory: host.update(ansible_config) gateway = None - if asset.domain: - gateway = asset.domain.select_gateway() + domains = asset.get_domains() + if domains: + gateways = [i.select_gateway() for i in domains if i.select_gateway()] + gateway = random.choice(gateways) if gateways else None if ansible_connection == 'local': if gateway: From 426900145e12af820637080ab764032653f0080c Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 29 Nov 2022 17:08:15 +0800 Subject: [PATCH 30/65] perf: connect token asset add specific --- apps/authentication/serializers/connection_token.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/authentication/serializers/connection_token.py b/apps/authentication/serializers/connection_token.py index 5915f542b..e851ceadf 100644 --- a/apps/authentication/serializers/connection_token.py +++ b/apps/authentication/serializers/connection_token.py @@ -89,7 +89,8 @@ class ConnectionTokenAssetSerializer(serializers.ModelSerializer): class Meta: model = Asset - fields = ['id', 'name', 'address', 'protocols', 'org_id'] + fields = ['id', 'name', 'address', 'protocols', + 'org_id', 'specific'] class ConnectionTokenAccountSerializer(serializers.ModelSerializer): From 65936485548b93d21ca7c971d9040c4e1b0f17bd Mon Sep 17 00:00:00 2001 From: ibuler Date: Tue, 29 Nov 2022 18:36:42 +0800 Subject: [PATCH 31/65] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=20connect=20to?= =?UTF-8?q?ken?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/api/connection_token.py | 49 +++++++++++++-------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 387081cc6..3e1a61a86 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -20,7 +20,7 @@ from common.utils.django import get_request_os from orgs.mixins.api import RootOrgViewMixin from perms.models import ActionChoices from terminal.const import NativeClient -from terminal.models import EndpointRule +from terminal.models import EndpointRule, Applet from ..models import ConnectionToken from ..serializers import ( ConnectionTokenSerializer, ConnectionTokenSecretSerializer, @@ -33,13 +33,34 @@ class RDPFileClientProtocolURLMixin: request: Request get_serializer: callable + @staticmethod + def set_applet_info(token, rdp_options): + # remote-app + applet = Applet.objects.filter(name=token.connect_method).first() + if not applet: + return rdp_options + + cmdline = { + 'app_name': applet.name, + 'user_id': str(token.user.id), + 'asset_id': str(token.asset.id), + 'token_id': token.id + } + + app = '||tinker' + rdp_options['remoteapplicationmode:i'] = '1' + rdp_options['alternate shell:s'] = app + rdp_options['remoteapplicationprogram:s'] = app + rdp_options['remoteapplicationname:s'] = app + + cmdline_b64 = base64.b64encode(json.dumps(cmdline).encode()).decode() + rdp_options['remoteapplicationcmdline:s'] = cmdline_b64 + return rdp_options + def get_rdp_file_info(self, token: ConnectionToken): rdp_options = { 'full address:s': '', 'username:s': '', - # 'screen mode id:i': '1', - # 'desktopwidth:i': '1280', - # 'desktopheight:i': '800', 'use multimon:i': '0', 'session bpp:i': '32', 'audiomode:i': '0', @@ -60,11 +81,6 @@ class RDPFileClientProtocolURLMixin: 'bookmarktype:i': '3', 'use redirection server name:i': '0', 'smart sizing:i': '1', - # 'drivestoredirect:s': '*', - # 'domain:s': '' - # 'alternate shell:s:': '||MySQLWorkbench', - # 'remoteapplicationname:s': 'Firefox', - # 'remoteapplicationcmdline:s': '', } # 设置磁盘挂载 @@ -97,16 +113,11 @@ class RDPFileClientProtocolURLMixin: rdp_options['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32') rdp_options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0') - if token.asset: - name = token.asset.name - # remote-app - # app = '||jmservisor' - # rdp_options['remoteapplicationmode:i'] = '1' - # rdp_options['alternate shell:s'] = app - # rdp_options['remoteapplicationprogram:s'] = app - # rdp_options['remoteapplicationname:s'] = name - else: - name = '*' + # 设置远程应用 + self.set_applet_info(token, rdp_options) + + # 文件名 + name = token.asset.name prefix_name = f'{token.user.username}-{name}' filename = self.get_connect_filename(prefix_name) From 52541d1dad94b40eda791a472b7eab1794b9e13a Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Tue, 29 Nov 2022 19:05:45 +0800 Subject: [PATCH 32/65] perf: push dynamic user --- apps/assets/tasks/push_account.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/assets/tasks/push_account.py b/apps/assets/tasks/push_account.py index c2c7156e8..7c596c8f2 100644 --- a/apps/assets/tasks/push_account.py +++ b/apps/assets/tasks/push_account.py @@ -12,11 +12,14 @@ __all__ = [ @org_aware_func("assets") -def push_accounts_to_assets_util(accounts, assets): +def push_accounts_to_assets_util(accounts, assets, username=None): from assets.models import PushAccountAutomation task_name = gettext_noop("Push accounts to assets") task_name = PushAccountAutomation.generate_unique_name(task_name) - account_usernames = list(accounts.values_list('username', flat=True)) + if username is None: + account_usernames = list(accounts.values_list('username', flat=True)) + else: + account_usernames = [username] data = { 'name': task_name, @@ -29,10 +32,10 @@ def push_accounts_to_assets_util(accounts, assets): @shared_task(queue="ansible", verbose_name=_('Push accounts to assets')) -def push_accounts_to_assets(account_ids, asset_ids): +def push_accounts_to_assets(account_ids, asset_ids, username=None): from assets.models import Asset, Account with tmp_to_root_org(): assets = Asset.objects.filter(id__in=asset_ids) accounts = Account.objects.filter(id__in=account_ids) - return push_accounts_to_assets_util(accounts, assets) + return push_accounts_to_assets_util(accounts, assets, username) From dd207016b2837e20651bdce6a8b7582a9879d577 Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Tue, 29 Nov 2022 19:14:12 +0800 Subject: [PATCH 33/65] perf: del domain node --- apps/assets/migrations/0114_node_domain.py | 9 +-------- apps/assets/models/asset/common.py | 8 -------- apps/assets/models/node.py | 4 ---- apps/assets/serializers/domain.py | 10 +--------- apps/ops/ansible/inventory.py | 7 ++----- 5 files changed, 4 insertions(+), 34 deletions(-) diff --git a/apps/assets/migrations/0114_node_domain.py b/apps/assets/migrations/0114_node_domain.py index 896a611d5..0149a5d11 100644 --- a/apps/assets/migrations/0114_node_domain.py +++ b/apps/assets/migrations/0114_node_domain.py @@ -4,18 +4,11 @@ from django.db import migrations, models import django.db.models.deletion +# TODO 最后去掉这个迁移 class Migration(migrations.Migration): dependencies = [ ('assets', '0113_alter_accounttemplate_options'), ] operations = [ - migrations.AddField( - model_name='node', - name='domain', - field=models.ForeignKey( - blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, - related_name='nodes', to='assets.domain', verbose_name='Domain' - ), - ), ] diff --git a/apps/assets/models/asset/common.py b/apps/assets/models/asset/common.py index 7f01c222d..81693252d 100644 --- a/apps/assets/models/asset/common.py +++ b/apps/assets/models/asset/common.py @@ -105,14 +105,6 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel): def __str__(self): return '{0.name}({0.address})'.format(self) - def get_domains(self): - from ..domain import Domain - node_ids = self.get_all_nodes(flat=True) - domains = Domain.objects.filter( - Q(nodes__id__in=node_ids) | Q(id=self.domain_id) - ).distinct() - return domains - @property def specific(self): if not hasattr(self, self.category): diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index bf394e982..54bdecf61 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -558,10 +558,6 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin): max_length=64, verbose_name=_("Parent key"), db_index=True, default='' ) assets_amount = models.IntegerField(default=0) - domain = models.ForeignKey( - "assets.Domain", null=True, blank=True, related_name='nodes', - verbose_name=_("Domain"), on_delete=models.SET_NULL - ) objects = OrgManager.from_queryset(NodeQuerySet)() is_node = True diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py index 617aa34bc..f60cb9516 100644 --- a/apps/assets/serializers/domain.py +++ b/apps/assets/serializers/domain.py @@ -14,12 +14,8 @@ from .utils import validate_password_for_ansible, validate_ssh_key class DomainSerializer(BulkOrgResourceModelSerializer): - node_count = serializers.SerializerMethodField(label=_('Nodes amount')) asset_count = serializers.SerializerMethodField(label=_('Assets amount')) gateway_count = serializers.SerializerMethodField(label=_('Gateways count')) - nodes = ObjectRelatedField( - many=True, required=False, queryset=Node.objects, label=_('Node') - ) assets = ObjectRelatedField( many=True, required=False, queryset=Asset.objects, label=_('Asset') ) @@ -29,17 +25,13 @@ class DomainSerializer(BulkOrgResourceModelSerializer): fields_mini = ['id', 'name'] fields_small = fields_mini + ['comment'] fields_m2m = ['nodes', 'assets'] - read_only_fields = ['node_count', 'asset_count', 'gateway_count', 'date_created'] + read_only_fields = ['asset_count', 'gateway_count', 'date_created'] fields = fields_small + fields_m2m + read_only_fields extra_kwargs = { 'assets': {'required': False, 'label': _('Assets')}, } - @staticmethod - def get_node_count(obj): - return obj.nodes.count() - @staticmethod def get_asset_count(obj): return obj.assets.count() diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py index aa7f8186d..1e006ae77 100644 --- a/apps/ops/ansible/inventory.py +++ b/apps/ops/ansible/inventory.py @@ -1,7 +1,6 @@ # ~*~ coding: utf-8 ~*~ import json import os -import random from collections import defaultdict from django.utils.translation import gettext as _ @@ -118,10 +117,8 @@ class JMSInventory: host.update(ansible_config) gateway = None - domains = asset.get_domains() - if domains: - gateways = [i.select_gateway() for i in domains if i.select_gateway()] - gateway = random.choice(gateways) if gateways else None + if asset.domain: + gateway = asset.domain.select_gateway() if ansible_connection == 'local': if gateway: From cc5b37350c9fdde531d13076ef0471c5ca0df803 Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Tue, 29 Nov 2022 19:37:11 +0800 Subject: [PATCH 34/65] perf: domain del nodes --- apps/assets/serializers/domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py index f60cb9516..3e2b4889f 100644 --- a/apps/assets/serializers/domain.py +++ b/apps/assets/serializers/domain.py @@ -24,7 +24,7 @@ class DomainSerializer(BulkOrgResourceModelSerializer): model = Domain fields_mini = ['id', 'name'] fields_small = fields_mini + ['comment'] - fields_m2m = ['nodes', 'assets'] + fields_m2m = ['assets'] read_only_fields = ['asset_count', 'gateway_count', 'date_created'] fields = fields_small + fields_m2m + read_only_fields From e191a197c6b71ed0be7a62b10cdc1af658401448 Mon Sep 17 00:00:00 2001 From: Aaron3S Date: Tue, 29 Nov 2022 19:44:12 +0800 Subject: [PATCH 35/65] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E5=89=A9?= =?UTF-8?q?=E4=BD=99=E6=B5=81=E7=A8=8B,=20=E4=BF=AE=E6=94=B9=E9=83=A8?= =?UTF-8?q?=E5=88=86=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/ops/api/adhoc.py | 7 +- apps/ops/api/job.py | 18 +++-- apps/ops/api/playbook.py | 8 ++- .../ops/migrations/0036_auto_20221129_1529.py | 26 ++++++++ .../ops/migrations/0037_auto_20221129_1926.py | 26 ++++++++ apps/ops/migrations/0038_playbook_org_id.py | 18 +++++ .../ops/migrations/0039_auto_20221129_1932.py | 25 +++++++ apps/ops/models/adhoc.py | 36 ++-------- apps/ops/models/job.py | 9 ++- apps/ops/models/playbook.py | 6 +- apps/ops/serializers/adhoc.py | 65 ++----------------- apps/ops/serializers/job.py | 3 +- apps/ops/serializers/playbook.py | 5 +- apps/ops/tasks.py | 11 +--- 14 files changed, 144 insertions(+), 119 deletions(-) create mode 100644 apps/ops/migrations/0036_auto_20221129_1529.py create mode 100644 apps/ops/migrations/0037_auto_20221129_1926.py create mode 100644 apps/ops/migrations/0038_playbook_org_id.py create mode 100644 apps/ops/migrations/0039_auto_20221129_1932.py diff --git a/apps/ops/api/adhoc.py b/apps/ops/api/adhoc.py index 9889349df..aca7047f1 100644 --- a/apps/ops/api/adhoc.py +++ b/apps/ops/api/adhoc.py @@ -2,6 +2,8 @@ # from rest_framework import viewsets + +from orgs.mixins.api import OrgBulkModelViewSet from ..models import AdHoc from ..serializers import ( AdHocSerializer @@ -12,6 +14,7 @@ __all__ = [ ] -class AdHocViewSet(viewsets.ModelViewSet): - queryset = AdHoc.objects.all() +class AdHocViewSet(OrgBulkModelViewSet): serializer_class = AdHocSerializer + permission_classes = () + model = AdHoc diff --git a/apps/ops/api/job.py b/apps/ops/api/job.py index e668bcee0..65d741b52 100644 --- a/apps/ops/api/job.py +++ b/apps/ops/api/job.py @@ -5,10 +5,16 @@ from ops.serializers.job import JobSerializer, JobExecutionSerializer __all__ = ['JobViewSet', 'JobExecutionViewSet'] -from ops.tasks import run_ops_job, run_ops_job_executions +from ops.tasks import run_ops_job_execution from orgs.mixins.api import OrgBulkModelViewSet +def set_task_to_serializer_data(serializer, task): + data = getattr(serializer, "_data", {}) + data["task_id"] = task.id + setattr(serializer, "_data", data) + + class JobViewSet(OrgBulkModelViewSet): serializer_class = JobSerializer model = Job @@ -23,23 +29,25 @@ class JobViewSet(OrgBulkModelViewSet): def perform_create(self, serializer): instance = serializer.save() if instance.instant: - run_ops_job.delay(instance.id) + execution = instance.create_execution() + task = run_ops_job_execution.delay(execution.id) + set_task_to_serializer_data(serializer, task) class JobExecutionViewSet(OrgBulkModelViewSet): serializer_class = JobExecutionSerializer http_method_names = ('get', 'post', 'head', 'options',) - # filter_fields = ('type',) permission_classes = () model = JobExecution def perform_create(self, serializer): instance = serializer.save() - run_ops_job_executions.delay(instance.id) + task = run_ops_job_execution.delay(instance.id) + set_task_to_serializer_data(serializer, task) def get_queryset(self): query_set = super().get_queryset() job_id = self.request.query_params.get('job_id') if job_id: - self.queryset = query_set.filter(job_id=job_id) + query_set = query_set.filter(job_id=job_id) return query_set diff --git a/apps/ops/api/playbook.py b/apps/ops/api/playbook.py index 2cb846c0e..bda7dfe33 100644 --- a/apps/ops/api/playbook.py +++ b/apps/ops/api/playbook.py @@ -2,7 +2,8 @@ import os import zipfile from django.conf import settings -from rest_framework import viewsets + +from orgs.mixins.api import OrgBulkModelViewSet from ..models import Playbook from ..serializers.playbook import PlaybookSerializer @@ -15,9 +16,10 @@ def unzip_playbook(src, dist): fz.extract(file, dist) -class PlaybookViewSet(viewsets.ModelViewSet): - queryset = Playbook.objects.all() +class PlaybookViewSet(OrgBulkModelViewSet): serializer_class = PlaybookSerializer + permission_classes = () + model = Playbook def perform_create(self, serializer): instance = serializer.save() diff --git a/apps/ops/migrations/0036_auto_20221129_1529.py b/apps/ops/migrations/0036_auto_20221129_1529.py new file mode 100644 index 000000000..a766ee72d --- /dev/null +++ b/apps/ops/migrations/0036_auto_20221129_1529.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.14 on 2022-11-29 07:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0035_jobexecution_org_id'), + ] + + operations = [ + migrations.AlterModelOptions( + name='job', + options={'ordering': ['date_created']}, + ), + migrations.AlterModelOptions( + name='jobexecution', + options={'ordering': ['-date_created']}, + ), + migrations.AddField( + model_name='job', + name='use_parameter_define', + field=models.BooleanField(default=False, verbose_name='Use Parameter Define'), + ), + ] diff --git a/apps/ops/migrations/0037_auto_20221129_1926.py b/apps/ops/migrations/0037_auto_20221129_1926.py new file mode 100644 index 000000000..086a93cd7 --- /dev/null +++ b/apps/ops/migrations/0037_auto_20221129_1926.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.14 on 2022-11-29 11:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0036_auto_20221129_1529'), + ] + + operations = [ + migrations.RenameField( + model_name='adhoc', + old_name='owner', + new_name='creator', + ), + migrations.AddField( + model_name='adhoc', + name='org_id', + field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), + ), + migrations.DeleteModel( + name='AdHocExecution', + ), + ] diff --git a/apps/ops/migrations/0038_playbook_org_id.py b/apps/ops/migrations/0038_playbook_org_id.py new file mode 100644 index 000000000..0de95334c --- /dev/null +++ b/apps/ops/migrations/0038_playbook_org_id.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2022-11-29 11:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0037_auto_20221129_1926'), + ] + + operations = [ + migrations.AddField( + model_name='playbook', + name='org_id', + field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), + ), + ] diff --git a/apps/ops/migrations/0039_auto_20221129_1932.py b/apps/ops/migrations/0039_auto_20221129_1932.py new file mode 100644 index 000000000..f54ed1cbe --- /dev/null +++ b/apps/ops/migrations/0039_auto_20221129_1932.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.14 on 2022-11-29 11:32 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('ops', '0038_playbook_org_id'), + ] + + operations = [ + migrations.RemoveField( + model_name='playbook', + name='owner', + ), + migrations.AddField( + model_name='playbook', + name='creator', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Creator'), + ), + ] diff --git a/apps/ops/models/adhoc.py b/apps/ops/models/adhoc.py index e94223fb9..2c0cf9940 100644 --- a/apps/ops/models/adhoc.py +++ b/apps/ops/models/adhoc.py @@ -1,21 +1,18 @@ # ~*~ coding: utf-8 ~*~ -import os.path import uuid from django.db import models from django.utils.translation import ugettext_lazy as _ -from common.db.models import BaseCreateUpdateModel from common.utils import get_logger -from .base import BaseAnsibleJob, BaseAnsibleExecution -from ..ansible import AdHocRunner +from orgs.mixins.models import JMSOrgBaseModel -__all__ = ["AdHoc", "AdHocExecution"] +__all__ = ["AdHoc"] logger = get_logger(__file__) -class AdHoc(BaseCreateUpdateModel): +class AdHoc(JMSOrgBaseModel): class Modules(models.TextChoices): shell = 'shell', _('Shell') winshell = 'win_shell', _('Powershell') @@ -26,7 +23,7 @@ class AdHoc(BaseCreateUpdateModel): module = models.CharField(max_length=128, choices=Modules.choices, default=Modules.shell, verbose_name=_('Module')) args = models.CharField(max_length=1024, default='', verbose_name=_('Args')) - owner = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True) + creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True) @property def row_count(self): @@ -41,28 +38,3 @@ class AdHoc(BaseCreateUpdateModel): def __str__(self): return "{}: {}".format(self.module, self.args) - - -class AdHocExecution(BaseAnsibleExecution): - """ - AdHoc running history. - """ - task = models.ForeignKey('AdHoc', verbose_name=_("Adhoc"), related_name='executions', on_delete=models.CASCADE) - - def get_runner(self): - inv = self.task.inventory - inv.write_to_file(self.inventory_path) - - runner = AdHocRunner( - self.inventory_path, self.task.module, module_args=self.task.args, - pattern=self.task.pattern, project_dir=self.private_dir - ) - return runner - - def task_display(self): - return str(self.task) - - class Meta: - db_table = "ops_adhoc_execution" - get_latest_by = 'date_start' - verbose_name = _("AdHoc execution") diff --git a/apps/ops/models/job.py b/apps/ops/models/job.py index 3795a0455..e74258b55 100644 --- a/apps/ops/models/job.py +++ b/apps/ops/models/job.py @@ -45,6 +45,7 @@ class Job(JMSOrgBaseModel, PeriodTaskModelMixin): runas = models.CharField(max_length=128, default='root', verbose_name=_('Runas')) runas_policy = models.CharField(max_length=128, choices=RunasPolicies.choices, default=RunasPolicies.skip, verbose_name=_('Runas policy')) + use_parameter_define = models.BooleanField(default=False, verbose_name=(_('Use Parameter Define'))) parameters_define = models.JSONField(default=dict, verbose_name=_('Parameters define')) comment = models.CharField(max_length=1024, default='', verbose_name=_('Comment'), null=True, blank=True) @@ -77,9 +78,9 @@ class Job(JMSOrgBaseModel, PeriodTaskModelMixin): return total_cost / finished_count if finished_count else 0 def get_register_task(self): - from ..tasks import run_ops_job + from ..tasks import run_ops_job_execution name = "run_ops_job_period_{}".format(str(self.id)[:8]) - task = run_ops_job.name + task = run_ops_job_execution.name args = (str(self.id),) kwargs = {} return name, task, args, kwargs @@ -108,6 +109,10 @@ class JobExecution(JMSOrgBaseModel): date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True) date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished")) + @property + def job_type(self): + return self.job.type + def get_runner(self): inv = self.job.inventory inv.write_to_file(self.inventory_path) diff --git a/apps/ops/models/playbook.py b/apps/ops/models/playbook.py index 10be7bd06..9af396124 100644 --- a/apps/ops/models/playbook.py +++ b/apps/ops/models/playbook.py @@ -5,14 +5,14 @@ from django.conf import settings from django.db import models from django.utils.translation import gettext_lazy as _ -from common.db.models import BaseCreateUpdateModel +from orgs.mixins.models import JMSOrgBaseModel -class Playbook(BaseCreateUpdateModel): +class Playbook(JMSOrgBaseModel): id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, verbose_name=_('Name'), null=True) path = models.FileField(upload_to='playbooks/') - owner = models.ForeignKey('users.User', verbose_name=_("Owner"), on_delete=models.SET_NULL, null=True) + creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True) @property def work_path(self): diff --git a/apps/ops/serializers/adhoc.py b/apps/ops/serializers/adhoc.py index f5d8d4780..fdbb9a397 100644 --- a/apps/ops/serializers/adhoc.py +++ b/apps/ops/serializers/adhoc.py @@ -6,70 +6,15 @@ import datetime from rest_framework import serializers from common.drf.fields import ReadableHiddenField -from ..models import AdHoc, AdHocExecution +from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from ..models import AdHoc -class AdHocSerializer(serializers.ModelSerializer): - owner = ReadableHiddenField(default=serializers.CurrentUserDefault()) +class AdHocSerializer(BulkOrgResourceModelSerializer, serializers.ModelSerializer): + creator = ReadableHiddenField(default=serializers.CurrentUserDefault()) row_count = serializers.IntegerField(read_only=True) size = serializers.IntegerField(read_only=True) class Meta: model = AdHoc - fields = ["id", "name", "module", "row_count", "size", "args", "owner", "date_created", "date_updated"] - - -class AdHocExecutionSerializer(serializers.ModelSerializer): - stat = serializers.SerializerMethodField() - last_success = serializers.ListField(source='success_hosts') - last_failure = serializers.DictField(source='failed_hosts') - - class Meta: - model = AdHocExecution - fields_mini = ['id'] - fields_small = fields_mini + [ - 'timedelta', 'result', 'summary', 'short_id', - 'is_finished', 'is_success', - 'date_start', 'date_finished', - ] - fields_fk = ['task', 'task_display'] - fields_custom = ['stat', 'last_success', 'last_failure'] - fields = fields_small + fields_fk + fields_custom - - @staticmethod - def get_task(obj): - return obj.task.id - - @staticmethod - def get_stat(obj): - count_failed_hosts = len(obj.failed_hosts) - count_success_hosts = len(obj.success_hosts) - count_total = count_success_hosts + count_failed_hosts - return { - "total": count_total, - "success": count_success_hosts, - "failed": count_failed_hosts - } - - -class AdHocExecutionExcludeResultSerializer(AdHocExecutionSerializer): - class Meta: - model = AdHocExecution - fields = [ - 'id', 'task', 'task_display', 'hosts_amount', 'adhoc', 'date_start', 'stat', - 'date_finished', 'timedelta', 'is_finished', 'is_success', - 'short_id', 'adhoc_short_id', 'last_success', 'last_failure' - ] - - -class AdHocExecutionNestSerializer(serializers.ModelSerializer): - last_success = serializers.ListField(source='success_hosts') - last_failure = serializers.DictField(source='failed_hosts') - last_run = serializers.CharField(source='short_id') - - class Meta: - model = AdHocExecution - fields = ( - 'last_success', 'last_failure', 'last_run', 'timedelta', - 'is_finished', 'is_success' - ) + fields = ["id", "name", "module", "row_count", "size", "args", "creator", "date_created", "date_updated"] diff --git a/apps/ops/serializers/job.py b/apps/ops/serializers/job.py index e5d76f85b..5775c7094 100644 --- a/apps/ops/serializers/job.py +++ b/apps/ops/serializers/job.py @@ -15,6 +15,7 @@ class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin): read_only_fields = ["id", "date_last_run", "date_created", "date_updated", "average_time_cost"] fields = read_only_fields + [ "name", "instant", "type", "module", "args", "playbook", "assets", "runas_policy", "runas", "owner", + "use_parameter_define", "parameters_define", "timeout", "chdir", @@ -28,7 +29,7 @@ class JobExecutionSerializer(serializers.ModelSerializer): class Meta: model = JobExecution read_only_fields = ["id", "task_id", "timedelta", "time_cost", 'is_finished', 'date_start', 'date_created', - 'is_success', 'task_id', 'short_id'] + 'is_success', 'task_id', 'short_id', 'job_type'] fields = read_only_fields + [ "job", "parameters" ] diff --git a/apps/ops/serializers/playbook.py b/apps/ops/serializers/playbook.py index 7ca165501..0dc1c458e 100644 --- a/apps/ops/serializers/playbook.py +++ b/apps/ops/serializers/playbook.py @@ -4,6 +4,7 @@ from rest_framework import serializers from common.drf.fields import ReadableHiddenField from ops.models import Playbook +from orgs.mixins.serializers import BulkOrgResourceModelSerializer def parse_playbook_name(path): @@ -11,8 +12,8 @@ def parse_playbook_name(path): return file_name.split(".")[-2] -class PlaybookSerializer(serializers.ModelSerializer): - owner = ReadableHiddenField(default=serializers.CurrentUserDefault()) +class PlaybookSerializer(BulkOrgResourceModelSerializer, serializers.ModelSerializer): + creator = ReadableHiddenField(default=serializers.CurrentUserDefault()) def create(self, validated_data): name = validated_data.get('name') diff --git a/apps/ops/tasks.py b/apps/ops/tasks.py index 51541dd32..bd4d5d448 100644 --- a/apps/ops/tasks.py +++ b/apps/ops/tasks.py @@ -30,18 +30,11 @@ def run_ops_job(job_id): job = get_object_or_none(Job, id=job_id) with tmp_to_org(job.org): execution = job.create_execution() - try: - execution.start() - except SoftTimeLimitExceeded: - execution.set_error('Run timeout') - logger.error("Run adhoc timeout") - except Exception as e: - execution.set_error(e) - logger.error("Start adhoc execution error: {}".format(e)) + run_ops_job_execution(execution) @shared_task(soft_time_limit=60, queue="ansible", verbose_name=_("Run ansible task execution")) -def run_ops_job_executions(execution_id, **kwargs): +def run_ops_job_execution(execution_id, **kwargs): execution = get_object_or_none(JobExecution, id=execution_id) with tmp_to_org(execution.org): try: From d741f1434266fa62c089db912c26e5d200921651 Mon Sep 17 00:00:00 2001 From: Aaron3S Date: Tue, 29 Nov 2022 19:47:45 +0800 Subject: [PATCH 36/65] feat: merge migrations --- ...6_auto_20221128_1839_0039_auto_20221129_1932.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 apps/ops/migrations/0040_merge_0036_auto_20221128_1839_0039_auto_20221129_1932.py diff --git a/apps/ops/migrations/0040_merge_0036_auto_20221128_1839_0039_auto_20221129_1932.py b/apps/ops/migrations/0040_merge_0036_auto_20221128_1839_0039_auto_20221129_1932.py new file mode 100644 index 000000000..c0bbcb9a1 --- /dev/null +++ b/apps/ops/migrations/0040_merge_0036_auto_20221128_1839_0039_auto_20221129_1932.py @@ -0,0 +1,14 @@ +# Generated by Django 3.2.14 on 2022-11-29 11:47 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0036_auto_20221128_1839'), + ('ops', '0039_auto_20221129_1932'), + ] + + operations = [ + ] From 82a8118ca0f14e5e77f766213a99939514f952d4 Mon Sep 17 00:00:00 2001 From: Aaron3S Date: Tue, 29 Nov 2022 19:52:48 +0800 Subject: [PATCH 37/65] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0comment=20?= =?UTF-8?q?=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ops/migrations/0041_auto_20221129_1952.py | 23 +++++++++++++++++++ apps/ops/models/adhoc.py | 2 ++ apps/ops/models/playbook.py | 1 + apps/ops/serializers/adhoc.py | 3 ++- apps/ops/serializers/playbook.py | 2 +- 5 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 apps/ops/migrations/0041_auto_20221129_1952.py diff --git a/apps/ops/migrations/0041_auto_20221129_1952.py b/apps/ops/migrations/0041_auto_20221129_1952.py new file mode 100644 index 000000000..fb3411a4d --- /dev/null +++ b/apps/ops/migrations/0041_auto_20221129_1952.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.14 on 2022-11-29 11:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0040_merge_0036_auto_20221128_1839_0039_auto_20221129_1932'), + ] + + operations = [ + migrations.AddField( + model_name='adhoc', + name='comment', + field=models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment'), + ), + migrations.AddField( + model_name='playbook', + name='comment', + field=models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment'), + ), + ] diff --git a/apps/ops/models/adhoc.py b/apps/ops/models/adhoc.py index 2c0cf9940..890a47b91 100644 --- a/apps/ops/models/adhoc.py +++ b/apps/ops/models/adhoc.py @@ -24,6 +24,8 @@ class AdHoc(JMSOrgBaseModel): verbose_name=_('Module')) args = models.CharField(max_length=1024, default='', verbose_name=_('Args')) creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True) + comment = models.CharField(max_length=1024, default='', verbose_name=_('Comment'), null=True, blank=True) + @property def row_count(self): diff --git a/apps/ops/models/playbook.py b/apps/ops/models/playbook.py index 9af396124..eb767649a 100644 --- a/apps/ops/models/playbook.py +++ b/apps/ops/models/playbook.py @@ -13,6 +13,7 @@ class Playbook(JMSOrgBaseModel): name = models.CharField(max_length=128, verbose_name=_('Name'), null=True) path = models.FileField(upload_to='playbooks/') creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True) + comment = models.CharField(max_length=1024, default='', verbose_name=_('Comment'), null=True, blank=True) @property def work_path(self): diff --git a/apps/ops/serializers/adhoc.py b/apps/ops/serializers/adhoc.py index fdbb9a397..48ddf6567 100644 --- a/apps/ops/serializers/adhoc.py +++ b/apps/ops/serializers/adhoc.py @@ -17,4 +17,5 @@ class AdHocSerializer(BulkOrgResourceModelSerializer, serializers.ModelSerialize class Meta: model = AdHoc - fields = ["id", "name", "module", "row_count", "size", "args", "creator", "date_created", "date_updated"] + fields = ["id", "name", "module", "row_count", "size", "args", "creator", "comment", "date_created", + "date_updated"] diff --git a/apps/ops/serializers/playbook.py b/apps/ops/serializers/playbook.py index 0dc1c458e..57c7f2fe5 100644 --- a/apps/ops/serializers/playbook.py +++ b/apps/ops/serializers/playbook.py @@ -25,5 +25,5 @@ class PlaybookSerializer(BulkOrgResourceModelSerializer, serializers.ModelSerial class Meta: model = Playbook fields = [ - "id", "name", "path", "date_created", "owner", "date_updated" + "id", "name", "path", "comment", "date_created", "creator", "date_updated" ] From 44ee80b05a78bb4559eb5b9e5877a620f3b4491a Mon Sep 17 00:00:00 2001 From: ibuler Date: Tue, 29 Nov 2022 21:41:33 +0800 Subject: [PATCH 38/65] =?UTF-8?q?perf:=20=E5=8E=BB=E6=8E=89=20connect=20to?= =?UTF-8?q?ken=20endpoint=20protocol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/api/connection_token.py | 23 ++++++++++++------- ...emove_connectiontoken_endpoint_protocol.py | 17 ++++++++++++++ .../authentication/models/connection_token.py | 3 --- .../serializers/connection_token.py | 2 +- apps/terminal/const.py | 9 ++++++++ 5 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 apps/authentication/migrations/0019_remove_connectiontoken_endpoint_protocol.py diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 3e1a61a86..67c732519 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -19,12 +19,13 @@ from common.utils import random_string from common.utils.django import get_request_os from orgs.mixins.api import RootOrgViewMixin from perms.models import ActionChoices -from terminal.const import NativeClient +from terminal.const import NativeClient, TerminalType from terminal.models import EndpointRule, Applet from ..models import ConnectionToken from ..serializers import ( ConnectionTokenSerializer, ConnectionTokenSecretSerializer, - SuperConnectionTokenSerializer, ) + SuperConnectionTokenSerializer, +) __all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet'] @@ -143,9 +144,12 @@ class RDPFileClientProtocolURLMixin: def get_client_protocol_data(self, token: ConnectionToken): _os = get_request_os(self.request) - connect_method = getattr(NativeClient, token.connect_method, None) - if connect_method is None: - raise ValueError('Connect method not support: {}'.format(token.connect_method)) + connect_method_name = token.connect_method + connect_method_dict = TerminalType.get_connect_method( + token.connect_method, token.protocol, _os + ) + if connect_method_dict is None: + raise ValueError('Connect method not support: {}'.format(connect_method_name)) data = { 'id': str(token.id), @@ -154,7 +158,7 @@ class RDPFileClientProtocolURLMixin: 'file': {} } - if connect_method == NativeClient.mstsc: + if connect_method_name == NativeClient.mstsc: filename, content = self.get_rdp_file_info(token) data.update({ 'file': { @@ -163,8 +167,11 @@ class RDPFileClientProtocolURLMixin: } }) else: - endpoint = self.get_smart_endpoint(protocol=token.endpoint_protocol, asset=token.asset) - cmd = NativeClient.get_launch_command(connect_method, token, endpoint) + endpoint = self.get_smart_endpoint( + protocol=connect_method_dict['endpoint_protocol'], + asset=token.asset + ) + cmd = NativeClient.get_launch_command(connect_method_name, token, endpoint) data.update({'command': cmd}) return data diff --git a/apps/authentication/migrations/0019_remove_connectiontoken_endpoint_protocol.py b/apps/authentication/migrations/0019_remove_connectiontoken_endpoint_protocol.py new file mode 100644 index 000000000..ef7f401bd --- /dev/null +++ b/apps/authentication/migrations/0019_remove_connectiontoken_endpoint_protocol.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.14 on 2022-11-29 13:27 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0018_connectiontoken_endpoint_protocol'), + ] + + operations = [ + migrations.RemoveField( + model_name='connectiontoken', + name='endpoint_protocol', + ), + ] diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py index 7f4e7f42b..5505f81a3 100644 --- a/apps/authentication/models/connection_token.py +++ b/apps/authentication/models/connection_token.py @@ -35,9 +35,6 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel): choices=Protocol.choices, max_length=16, default=Protocol.ssh, verbose_name=_("Protocol") ) connect_method = models.CharField(max_length=32, verbose_name=_("Connect method")) - endpoint_protocol = models.CharField( - choices=Protocol.choices, max_length=16, verbose_name=_("Endpoint protocol") - ) user_display = models.CharField(max_length=128, default='', verbose_name=_("User display")) asset_display = models.CharField(max_length=128, default='', verbose_name=_("Asset display")) date_expired = models.DateTimeField( diff --git a/apps/authentication/serializers/connection_token.py b/apps/authentication/serializers/connection_token.py index e851ceadf..16ef7dc1b 100644 --- a/apps/authentication/serializers/connection_token.py +++ b/apps/authentication/serializers/connection_token.py @@ -23,7 +23,7 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): fields_small = fields_mini + [ 'user', 'asset', 'account_name', 'input_username', 'input_secret', - 'connect_method', 'endpoint_protocol', 'protocol', + 'connect_method', 'protocol', 'actions', 'date_expired', 'date_created', 'date_updated', 'created_by', 'updated_by', 'org_id', 'org_name', diff --git a/apps/terminal/const.py b/apps/terminal/const.py index 40c89ff24..dbfcc0dec 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -204,6 +204,15 @@ class TerminalType(TextChoices): } return protocols + @classmethod + def get_connect_method(cls, name, protocol, os): + methods = cls.get_protocols_connect_methods(os) + protocol_methods = methods.get(protocol, []) + for method in protocol_methods: + if method['value'] == name: + return method + return None + @classmethod def get_protocols_connect_methods(cls, os): methods = defaultdict(list) From 4b61790a923185d1d95e5883b77fb72a5cf3f3e9 Mon Sep 17 00:00:00 2001 From: ibuler Date: Tue, 29 Nov 2022 21:50:48 +0800 Subject: [PATCH 39/65] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20connect=20tok?= =?UTF-8?q?en=20remote=20app=20=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/api/connection_token.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 67c732519..6293c4627 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -45,7 +45,7 @@ class RDPFileClientProtocolURLMixin: 'app_name': applet.name, 'user_id': str(token.user.id), 'asset_id': str(token.asset.id), - 'token_id': token.id + 'token_id': str(token.id) } app = '||tinker' From 0f1e19ba41490d6dbab1da4353ea76ba9435c46b Mon Sep 17 00:00:00 2001 From: ibuler Date: Wed, 30 Nov 2022 11:28:51 +0800 Subject: [PATCH 40/65] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E6=B7=BB=E5=8A=A0=20ssl=20=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/api/mixin.py | 19 ++-------- .../migrations/0115_auto_20221130_1118.py | 38 +++++++++++++++++++ apps/assets/models/asset/database.py | 10 +++++ 3 files changed, 52 insertions(+), 15 deletions(-) create mode 100644 apps/assets/migrations/0115_auto_20221130_1118.py diff --git a/apps/assets/api/mixin.py b/apps/assets/api/mixin.py index f7f788e72..59be2b5f5 100644 --- a/apps/assets/api/mixin.py +++ b/apps/assets/api/mixin.py @@ -1,10 +1,10 @@ from typing import List + from rest_framework.request import Request -from common.utils import lazyproperty, timeit -from assets.models import Node, Asset -from assets.pagination import NodeAssetTreePagination +from assets.models import Node from assets.utils import get_node_from_request, is_query_node_all_assets +from common.utils import lazyproperty, timeit class SerializeToTreeNodeMixin: @@ -38,14 +38,6 @@ class SerializeToTreeNodeMixin: ] return data - def get_platform(self, asset: Asset): - default = 'file' - icon = {'windows', 'linux'} - platform = asset.platform.type.lower() - if platform in icon: - return platform - return default - @timeit def serialize_assets(self, assets, node_key=None): if node_key is None: @@ -61,13 +53,11 @@ class SerializeToTreeNodeMixin: 'pId': get_pid(asset), 'isParent': False, 'open': False, - 'iconSkin': self.get_platform(asset), + 'iconSkin': asset.type, 'chkDisabled': not asset.is_active, 'meta': { 'type': 'asset', 'data': { - 'id': asset.id, - 'name': asset.name, 'org_name': asset.org_name }, } @@ -78,7 +68,6 @@ class SerializeToTreeNodeMixin: class NodeFilterMixin: - # pagination_class = NodeAssetTreePagination request: Request @lazyproperty diff --git a/apps/assets/migrations/0115_auto_20221130_1118.py b/apps/assets/migrations/0115_auto_20221130_1118.py new file mode 100644 index 000000000..3d5fc9b20 --- /dev/null +++ b/apps/assets/migrations/0115_auto_20221130_1118.py @@ -0,0 +1,38 @@ +# Generated by Django 3.2.14 on 2022-11-30 03:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0114_node_domain'), + ] + + operations = [ + migrations.AddField( + model_name='database', + name='allow_invalid_cert', + field=models.BooleanField(default=False, verbose_name='Allow invalid cert'), + ), + migrations.AddField( + model_name='database', + name='ca_cert', + field=models.TextField(blank=True, verbose_name='CA cert'), + ), + migrations.AddField( + model_name='database', + name='client_cert', + field=models.TextField(blank=True, verbose_name='Client cert'), + ), + migrations.AddField( + model_name='database', + name='client_key', + field=models.TextField(blank=True, verbose_name='Client key'), + ), + migrations.AddField( + model_name='database', + name='use_ssl', + field=models.BooleanField(default=False, verbose_name='Use SSL'), + ), + ] diff --git a/apps/assets/models/asset/database.py b/apps/assets/models/asset/database.py index 6aef15a8f..4772a6b08 100644 --- a/apps/assets/models/asset/database.py +++ b/apps/assets/models/asset/database.py @@ -6,6 +6,11 @@ from .common import Asset class Database(Asset): db_name = models.CharField(max_length=1024, verbose_name=_("Database"), blank=True) + use_ssl = models.BooleanField(default=False, verbose_name=_("Use SSL")) + ca_cert = models.TextField(verbose_name=_("CA cert"), blank=True) + client_cert = models.TextField(verbose_name=_("Client cert"), blank=True) + client_key = models.TextField(verbose_name=_("Client key"), blank=True) + allow_invalid_cert = models.BooleanField(default=False, verbose_name=_('Allow invalid cert')) def __str__(self): return '{}({}://{}/{})'.format(self.name, self.type, self.address, self.db_name) @@ -18,6 +23,11 @@ class Database(Asset): def specific(self): return { 'db_name': self.db_name, + 'use_ssl': self.use_ssl, + 'ca_cert': self.ca_cert, + 'client_cert': self.client_cert, + 'client_key': self.client_key, + 'allow_invalid_cert': self.allow_invalid_cert, } class Meta: From 99e126f5157b5187393ad08ae5affbeed0bb194b Mon Sep 17 00:00:00 2001 From: Bai Date: Wed, 30 Nov 2022 15:08:55 +0800 Subject: [PATCH 41/65] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=20Acl=20accoun?= =?UTF-8?q?ts=20serializer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/acls/migrations/0004_auto_20220831_1658.py | 2 +- apps/acls/models/login_asset_acl.py | 2 +- apps/acls/serializers/login_asset_acl.py | 17 +++++------------ 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/apps/acls/migrations/0004_auto_20220831_1658.py b/apps/acls/migrations/0004_auto_20220831_1658.py index e4392992b..6fd1ef86b 100644 --- a/apps/acls/migrations/0004_auto_20220831_1658.py +++ b/apps/acls/migrations/0004_auto_20220831_1658.py @@ -22,7 +22,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='loginassetacl', name='accounts', - field=models.JSONField(default=dict, verbose_name='Account'), + field=models.JSONField(verbose_name='Account'), ), migrations.RunPython(migrate_system_users_to_accounts), migrations.RemoveField( diff --git a/apps/acls/models/login_asset_acl.py b/apps/acls/models/login_asset_acl.py index b01e4aed1..1c7455fb2 100644 --- a/apps/acls/models/login_asset_acl.py +++ b/apps/acls/models/login_asset_acl.py @@ -18,7 +18,7 @@ class LoginAssetACL(BaseACL, OrgModelMixin): # 条件 users = models.JSONField(verbose_name=_('User')) - accounts = models.JSONField(verbose_name=_('Account'), default=dict) + accounts = models.JSONField(verbose_name=_('Account')) assets = models.JSONField(verbose_name=_('Asset')) # 动作 action = models.CharField( diff --git a/apps/acls/serializers/login_asset_acl.py b/apps/acls/serializers/login_asset_acl.py index 84bab6cc3..2a04b6c97 100644 --- a/apps/acls/serializers/login_asset_acl.py +++ b/apps/acls/serializers/login_asset_acl.py @@ -28,30 +28,25 @@ class LoginAssetACLAssestsSerializer(serializers.Serializer): ip_group_help_text = _( "Format for comma-delimited string, with * indicating a match all. " "Such as: " - "192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 " - "(Domain name support)" + "192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64" + " (Domain name support)" ) ip_group = serializers.ListField( default=["*"], child=serializers.CharField(max_length=1024), - label=_("IP"), + label=_("IP/Host"), help_text=ip_group_help_text, ) hostname_group = serializers.ListField( default=["*"], child=serializers.CharField(max_length=128), - label=_("Hostname"), + label=_("Name"), help_text=common_help_text, ) class LoginAssetACLAccountsSerializer(serializers.Serializer): - protocol_group_help_text = _( - "Format for comma-delimited string, with * indicating a match all. " - "Protocol options: {}" - ) - name_group = serializers.ListField( default=["*"], child=serializers.CharField(max_length=128), @@ -70,9 +65,7 @@ class LoginAssetACLSerializer(BulkOrgResourceModelSerializer): users = LoginAssetACLUsersSerializer() assets = LoginAssetACLAssestsSerializer() accounts = LoginAssetACLAccountsSerializer() - reviewers_amount = serializers.IntegerField( - read_only=True, source="reviewers.count" - ) + reviewers_amount = serializers.IntegerField(read_only=True, source="reviewers.count") action = LabeledChoiceField( choices=models.LoginAssetACL.ActionChoices.choices, label=_("Action") ) From d46f321f1a8789af175adcbe3db30fac5413439d Mon Sep 17 00:00:00 2001 From: ibuler Date: Wed, 30 Nov 2022 15:24:32 +0800 Subject: [PATCH 42/65] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/api/mixin.py | 8 ++++-- apps/terminal/const.py | 56 ++++++++++++++++++++++++++++++---------- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/apps/assets/api/mixin.py b/apps/assets/api/mixin.py index 59be2b5f5..2abe967b0 100644 --- a/apps/assets/api/mixin.py +++ b/apps/assets/api/mixin.py @@ -2,7 +2,7 @@ from typing import List from rest_framework.request import Request -from assets.models import Node +from assets.models import Node, PlatformProtocol from assets.utils import get_node_from_request, is_query_node_all_assets from common.utils import lazyproperty, timeit @@ -40,6 +40,9 @@ class SerializeToTreeNodeMixin: @timeit def serialize_assets(self, assets, node_key=None): + sftp_enabled_platform = PlatformProtocol.objects \ + .filter(name='ssh', setting__sftp_enabled=True) \ + .values_list('platform', flat=True).distinct() if node_key is None: get_pid = lambda asset: getattr(asset, 'parent_key', '') else: @@ -58,7 +61,8 @@ class SerializeToTreeNodeMixin: 'meta': { 'type': 'asset', 'data': { - 'org_name': asset.org_name + 'org_name': asset.org_name, + 'sftp': asset.platform_id in sftp_enabled_platform, }, } } diff --git a/apps/terminal/const.py b/apps/terminal/const.py index dbfcc0dec..5177f1372 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -44,9 +44,30 @@ class ComponentLoad(TextChoices): return set(dict(cls.choices).keys()) -class HttpMethod(TextChoices): +class WebMethod(TextChoices): web_gui = 'web_gui', 'Web GUI' web_cli = 'web_cli', 'Web CLI' + web_sftp = 'web_sftp', 'Web SFTP' + + @classmethod + def get_methods(cls): + return { + Protocol.ssh: [cls.web_cli, cls.web_sftp], + Protocol.telnet: [cls.web_cli], + Protocol.rdp: [cls.web_gui], + Protocol.vnc: [cls.web_gui], + + Protocol.mysql: [cls.web_cli, cls.web_gui], + Protocol.mariadb: [cls.web_cli, cls.web_gui], + Protocol.oracle: [cls.web_cli, cls.web_gui], + Protocol.postgresql: [cls.web_cli, cls.web_gui], + Protocol.sqlserver: [cls.web_cli, cls.web_gui], + Protocol.redis: [cls.web_cli], + Protocol.mongodb: [cls.web_cli], + + Protocol.k8s: [cls.web_gui], + Protocol.http: [] + } class NativeClient(TextChoices): @@ -67,6 +88,8 @@ class NativeClient(TextChoices): @classmethod def get_native_clients(cls): + # native client 关注的是 endpoint 的 protocol, + # 比如 telnet mysql, koko 都支持,到那时暴露的是 ssh 协议 clients = { Protocol.ssh: { 'default': [cls.ssh], @@ -162,7 +185,7 @@ class TerminalType(TextChoices): def protocols(cls): protocols = { cls.koko: { - 'web_method': HttpMethod.web_cli, + 'web_methods': [WebMethod.web_cli, WebMethod.web_sftp], 'listen': [Protocol.ssh, Protocol.http], 'support': [ Protocol.ssh, Protocol.telnet, @@ -174,7 +197,7 @@ class TerminalType(TextChoices): 'match': 'm2m' }, cls.omnidb: { - 'web_method': HttpMethod.web_gui, + 'web_methods': [WebMethod.web_gui], 'listen': [Protocol.http], 'support': [ Protocol.mysql, Protocol.postgresql, Protocol.oracle, @@ -183,7 +206,7 @@ class TerminalType(TextChoices): 'match': 'm2m' }, cls.lion: { - 'web_method': HttpMethod.web_gui, + 'web_methods': [WebMethod.web_gui], 'listen': [Protocol.http], 'support': [Protocol.rdp, Protocol.vnc], 'match': 'm2m' @@ -216,6 +239,7 @@ class TerminalType(TextChoices): @classmethod def get_protocols_connect_methods(cls, os): methods = defaultdict(list) + web_methods = WebMethod.get_methods() native_methods = NativeClient.get_methods(os) applet_methods = AppletMethod.get_methods() @@ -229,16 +253,6 @@ class TerminalType(TextChoices): listen = component_protocol['listen'] for listen_protocol in listen: - if listen_protocol == Protocol.http: - web_protocol = component_protocol['web_method'] - methods[protocol.value].append({ - 'value': web_protocol.value, - 'label': web_protocol.label, - 'endpoint_protocol': 'http', - 'type': 'web', - 'component': component.value, - }) - # Native method methods[protocol.value].extend([ { @@ -250,6 +264,20 @@ class TerminalType(TextChoices): for method in native_methods[listen_protocol] ]) + protocol_web_methods = set(web_methods.get(protocol, [])) \ + & set(component_protocol.get('web_methods', [])) + print("protocol_web_methods", protocol, protocol_web_methods) + methods[protocol.value].extend([ + { + 'component': component.value, + 'type': 'web', + 'endpoint_protocol': 'http', + 'value': method.value, + 'label': method.label, + } + for method in protocol_web_methods + ]) + for protocol, applet_methods in applet_methods.items(): for method in applet_methods: method['type'] = 'applet' From 307cf97ccb59b8d7b69be4811d976a617a13c299 Mon Sep 17 00:00:00 2001 From: ibuler Date: Wed, 30 Nov 2022 15:39:27 +0800 Subject: [PATCH 43/65] =?UTF-8?q?perf:=20=E6=8E=88=E6=9D=83=E7=9A=84?= =?UTF-8?q?=E8=B5=84=E4=BA=A7=E6=94=AF=E6=8C=81=E8=BF=87=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/api/asset/asset.py | 3 ++- apps/perms/api/user_permission/assets/mixin.py | 12 +++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/assets/api/asset/asset.py b/apps/assets/api/asset/asset.py index f6cc509b3..04da13061 100644 --- a/apps/assets/api/asset/asset.py +++ b/apps/assets/api/asset/asset.py @@ -6,8 +6,8 @@ from rest_framework.decorators import action from rest_framework.response import Response from assets import serializers -from assets.models import Asset from assets.filters import IpInFilterBackend, LabelFilterBackend, NodeFilterBackend +from assets.models import Asset from assets.tasks import ( push_accounts_to_assets, test_assets_connectivity_manual, update_assets_hardware_info_manual, verify_accounts_connectivity, @@ -24,6 +24,7 @@ __all__ = [ "AssetViewSet", "AssetTaskCreateApi", "AssetsTaskCreateApi", + 'AssetFilterSet' ] diff --git a/apps/perms/api/user_permission/assets/mixin.py b/apps/perms/api/user_permission/assets/mixin.py index e7a584ef4..e95ba7aa1 100644 --- a/apps/perms/api/user_permission/assets/mixin.py +++ b/apps/perms/api/user_permission/assets/mixin.py @@ -1,13 +1,14 @@ -from rest_framework.response import Response from rest_framework.request import Request +from rest_framework.response import Response -from common.utils import get_logger -from users.models import User +from assets.api.asset.asset import AssetFilterSet from assets.api.mixin import SerializeToTreeNodeMixin from assets.models import Asset, Node -from perms.pagination import NodeGrantedAssetPagination, AllGrantedAssetPagination +from common.utils import get_logger from perms import serializers +from perms.pagination import NodeGrantedAssetPagination, AllGrantedAssetPagination from perms.utils.user_permission import UserGrantedAssetsQueryUtils +from users.models import User logger = get_logger(__name__) @@ -32,7 +33,8 @@ class UserAllGrantedAssetsQuerysetMixin: only_fields = serializers.AssetGrantedSerializer.Meta.only_fields pagination_class = AllGrantedAssetPagination ordering_fields = ("name", "address") - ordering = ('name', ) + filterset_class = AssetFilterSet + ordering = ('name',) user: User From 48d29494046c9f8fa698e418d61bfa5c802aad6c Mon Sep 17 00:00:00 2001 From: Bai Date: Wed, 30 Nov 2022 16:21:27 +0800 Subject: [PATCH 44/65] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=20acl=20assets?= =?UTF-8?q?=20name/address?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/acls/models/login_asset_acl.py | 11 +++++---- apps/acls/serializers/login_asset_acl.py | 26 +++++++++------------- apps/acls/serializers/login_asset_check.py | 4 +--- 3 files changed, 16 insertions(+), 25 deletions(-) diff --git a/apps/acls/models/login_asset_acl.py b/apps/acls/models/login_asset_acl.py index 1c7455fb2..a27a730d0 100644 --- a/apps/acls/models/login_asset_acl.py +++ b/apps/acls/models/login_asset_acl.py @@ -61,19 +61,18 @@ class LoginAssetACL(BaseACL, OrgModelMixin): @classmethod def filter_asset(cls, asset, queryset): queryset = queryset.filter( - Q(assets__hostname_group__contains=asset.name) | - Q(assets__hostname_group__contains='*') + Q(assets__name_group__contains=asset.name) | + Q(assets__name_group__contains='*') ) - ids = [q.id for q in queryset if contains_ip(asset.address, q.assets.get('ip_group', []))] + ids = [ + q.id for q in queryset if contains_ip(asset.address, q.assets.get('address_group', [])) + ] queryset = cls.objects.filter(id__in=ids) return queryset @classmethod def filter_account(cls, account_username, queryset): queryset = queryset.filter( - Q(accounts__name_group__contains=account_username) | - Q(accounts__name_group__contains='*') - ).filter( Q(accounts__username_group__contains=account_username) | Q(accounts__username_group__contains='*') ) diff --git a/apps/acls/serializers/login_asset_acl.py b/apps/acls/serializers/login_asset_acl.py index 2a04b6c97..053771fb4 100644 --- a/apps/acls/serializers/login_asset_acl.py +++ b/apps/acls/serializers/login_asset_acl.py @@ -25,34 +25,28 @@ class LoginAssetACLUsersSerializer(serializers.Serializer): class LoginAssetACLAssestsSerializer(serializers.Serializer): - ip_group_help_text = _( + address_group_help_text = _( "Format for comma-delimited string, with * indicating a match all. " "Such as: " "192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64" " (Domain name support)" ) - ip_group = serializers.ListField( - default=["*"], - child=serializers.CharField(max_length=1024), - label=_("IP/Host"), - help_text=ip_group_help_text, - ) - hostname_group = serializers.ListField( - default=["*"], - child=serializers.CharField(max_length=128), - label=_("Name"), - help_text=common_help_text, - ) - - -class LoginAssetACLAccountsSerializer(serializers.Serializer): name_group = serializers.ListField( default=["*"], child=serializers.CharField(max_length=128), label=_("Name"), help_text=common_help_text, ) + address_group = serializers.ListField( + default=["*"], + child=serializers.CharField(max_length=1024), + label=_("IP/Host"), + help_text=address_group_help_text, + ) + + +class LoginAssetACLAccountsSerializer(serializers.Serializer): username_group = serializers.ListField( default=["*"], child=serializers.CharField(max_length=128), diff --git a/apps/acls/serializers/login_asset_check.py b/apps/acls/serializers/login_asset_check.py index 279feb3b6..49afda63a 100644 --- a/apps/acls/serializers/login_asset_check.py +++ b/apps/acls/serializers/login_asset_check.py @@ -37,9 +37,7 @@ class LoginAssetCheckSerializer(serializers.Serializer): def validate_account_username(self, account_username): asset_id = self.initial_data.get('asset_id') - account = Account.objects.filter( - username=account_username, asset_id=asset_id - ).first() + account = Account.objects.filter(username=account_username, asset_id=asset_id).first() if not account: error = 'Account username does not exist' raise serializers.ValidationError(error) From dcbdb0af4d11c6f0ed597c6e2b05205bc9af6a7e Mon Sep 17 00:00:00 2001 From: Aaron3S Date: Wed, 30 Nov 2022 16:24:17 +0800 Subject: [PATCH 45/65] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E7=9B=AE=E5=BD=95=E5=88=87=E6=8D=A2,=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E5=AD=97=E6=AE=B5=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/ops/models/job.py | 10 +++++++++- apps/ops/serializers/adhoc.py | 6 ++---- apps/ops/serializers/playbook.py | 5 +++-- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/apps/ops/models/job.py b/apps/ops/models/job.py index e74258b55..5ce5f38c8 100644 --- a/apps/ops/models/job.py +++ b/apps/ops/models/job.py @@ -113,6 +113,13 @@ class JobExecution(JMSOrgBaseModel): def job_type(self): return self.job.type + def compile_shell(self): + if self.job.type != 'adhoc': + return + result = "{}{}{} ".format('\'', self.job.args, '\'') + result += "chdir={}".format(self.job.chdir) + return result + def get_runner(self): inv = self.job.inventory inv.write_to_file(self.inventory_path) @@ -122,8 +129,9 @@ class JobExecution(JMSOrgBaseModel): extra_vars = {} if self.job.type == 'adhoc': + args = self.compile_shell() runner = AdHocRunner( - self.inventory_path, self.job.module, module_args=self.job.args, + self.inventory_path, self.job.module, module_args=args, pattern="all", project_dir=self.private_dir, extra_vars=extra_vars, ) elif self.job.type == 'playbook': diff --git a/apps/ops/serializers/adhoc.py b/apps/ops/serializers/adhoc.py index 48ddf6567..08d583be1 100644 --- a/apps/ops/serializers/adhoc.py +++ b/apps/ops/serializers/adhoc.py @@ -1,8 +1,6 @@ # ~*~ coding: utf-8 ~*~ from __future__ import unicode_literals -import datetime - from rest_framework import serializers from common.drf.fields import ReadableHiddenField @@ -17,5 +15,5 @@ class AdHocSerializer(BulkOrgResourceModelSerializer, serializers.ModelSerialize class Meta: model = AdHoc - fields = ["id", "name", "module", "row_count", "size", "args", "creator", "comment", "date_created", - "date_updated"] + read_only_field = ["id", "row_count", "size", "creator", "date_created", "date_updated"] + fields = read_only_field + ["id", "name", "module", "args", "comment"] diff --git a/apps/ops/serializers/playbook.py b/apps/ops/serializers/playbook.py index 57c7f2fe5..4ff43abb4 100644 --- a/apps/ops/serializers/playbook.py +++ b/apps/ops/serializers/playbook.py @@ -24,6 +24,7 @@ class PlaybookSerializer(BulkOrgResourceModelSerializer, serializers.ModelSerial class Meta: model = Playbook - fields = [ - "id", "name", "path", "comment", "date_created", "creator", "date_updated" + read_only_fields = ["id", "date_created", "date_updated"] + fields = read_only_fields + [ + "id", "name", "comment", "creator", ] From ed77d05bd8eb5ba55cfdf9141924feb6d78d9ee0 Mon Sep 17 00:00:00 2001 From: ibuler Date: Wed, 30 Nov 2022 16:33:22 +0800 Subject: [PATCH 46/65] =?UTF-8?q?pref:=20conneect=20token=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20su=20from?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/models/connection_token.py | 8 +++++--- apps/authentication/serializers/connection_token.py | 13 +++++++++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py index 5505f81a3..850a6c589 100644 --- a/apps/authentication/models/connection_token.py +++ b/apps/authentication/models/connection_token.py @@ -128,16 +128,18 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel): if self.account_name == '@INPUT' or not account: return { 'name': self.account_name, - 'username': self.username, + 'username': self.input_username, 'secret_type': 'password', - 'secret': self.secret + 'secret': self.input_secret, + 'su_from': None } else: return { 'name': account.name, 'username': account.username, 'secret_type': account.secret_type, - 'secret': account.secret or self.secret + 'secret': account.secret or self.input_secret, + 'su_from': account.su_from, } @lazyproperty diff --git a/apps/authentication/serializers/connection_token.py b/apps/authentication/serializers/connection_token.py index 16ef7dc1b..dec6676eb 100644 --- a/apps/authentication/serializers/connection_token.py +++ b/apps/authentication/serializers/connection_token.py @@ -93,13 +93,22 @@ class ConnectionTokenAssetSerializer(serializers.ModelSerializer): 'org_id', 'specific'] -class ConnectionTokenAccountSerializer(serializers.ModelSerializer): +class SimpleAccountSerializer(serializers.ModelSerializer): """ Account """ + class Meta: + model = Account + fields = ['name', 'username', 'secret_type', 'secret'] + + +class ConnectionTokenAccountSerializer(serializers.ModelSerializer): + """ Account """ + su_from = SimpleAccountSerializer(required=False, label=_('Su from')) + class Meta: model = Account fields = [ - 'name', 'username', 'secret_type', 'secret', + 'name', 'username', 'secret_type', 'secret', 'su_from', ] From 0ffea3855b604f71dcda30c6ae40ec157286fcef Mon Sep 17 00:00:00 2001 From: ibuler Date: Wed, 30 Nov 2022 16:39:27 +0800 Subject: [PATCH 47/65] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=20connect=20to?= =?UTF-8?q?ken=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/serializers/connection_token.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/authentication/serializers/connection_token.py b/apps/authentication/serializers/connection_token.py index dec6676eb..225b8c8db 100644 --- a/apps/authentication/serializers/connection_token.py +++ b/apps/authentication/serializers/connection_token.py @@ -148,9 +148,9 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin): expire_now = serializers.BooleanField(label=_('Expired now'), default=True) user = ConnectionTokenUserSerializer(read_only=True) asset = ConnectionTokenAssetSerializer(read_only=True) - platform = ConnectionTokenPlatform(read_only=True) account = ConnectionTokenAccountSerializer(read_only=True) gateway = ConnectionTokenGatewaySerializer(read_only=True) + platform = ConnectionTokenPlatform(read_only=True) # cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True) actions = ActionChoicesField() expire_at = serializers.IntegerField() @@ -158,7 +158,7 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin): class Meta: model = ConnectionToken fields = [ - 'id', 'value', 'user', 'asset', 'platform', 'account', + 'id', 'value', 'user', 'asset', 'account', 'platform', 'protocol', 'gateway', 'actions', 'expire_at', 'expire_now', ] extra_kwargs = { From 6bb706efcf59e85e667c3ff79246c187b8eaffdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E5=B0=8F=E7=99=BD?= <296015668@qq.com> Date: Tue, 29 Nov 2022 11:32:26 +0800 Subject: [PATCH 48/65] =?UTF-8?q?perf:=20=E6=B7=BB=E5=8A=A0=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E6=9E=84=E5=BB=BA=E6=B5=8B=E8=AF=95=20actions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/jms-build-test.yml | 32 ++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/jms-build-test.yml diff --git a/.github/workflows/jms-build-test.yml b/.github/workflows/jms-build-test.yml new file mode 100644 index 000000000..0f5309cac --- /dev/null +++ b/.github/workflows/jms-build-test.yml @@ -0,0 +1,32 @@ +name: "Run Build Test" +on: + push: + branches: + - pr@* + - repr@* + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: docker/setup-qemu-action@v2 + + - uses: docker/setup-buildx-action@v2 + + - uses: docker/build-push-action@v3 + with: + context: . + push: false + tags: jumpserver/core:test + file: Dockerfile + cache-from: type=gha + cache-to: type=gha,mode=max + + - uses: LouisBrunner/checks-action@v1.5.0 + if: always() + with: + token: ${{ secrets.GITHUB_TOKEN }} + name: Check Build + conclusion: ${{ job.status }} From 2dea891b1524ce91add4786b8756cf1be733090a Mon Sep 17 00:00:00 2001 From: Bai Date: Wed, 30 Nov 2022 17:11:36 +0800 Subject: [PATCH 49/65] =?UTF-8?q?perf:=20=E5=A4=84=E7=90=86=20acl=20?= =?UTF-8?q?=E5=90=8E=E5=8F=B0=20check=20=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/acls/api/login_asset_check.py | 46 +++++++++++----------- apps/acls/serializers/login_asset_check.py | 30 ++++---------- 2 files changed, 31 insertions(+), 45 deletions(-) diff --git a/apps/acls/api/login_asset_check.py b/apps/acls/api/login_asset_check.py index 331c42768..662befafd 100644 --- a/apps/acls/api/login_asset_check.py +++ b/apps/acls/api/login_asset_check.py @@ -20,34 +20,41 @@ class LoginAssetCheckAPI(CreateAPIView): return LoginAssetACL.objects.all() def create(self, request, *args, **kwargs): - is_need_confirm, response_data = self.check_if_need_confirm() - return Response(data=response_data, status=200) + data = self.check_confirm() + return Response(data=data, status=200) - def check_if_need_confirm(self): + @lazyproperty + def serializer(self): + serializer = self.get_serializer(data=self.request.data) + serializer.is_valid(raise_exception=True) + return serializer + + def check_confirm(self): queries = { - 'user': self.serializer.user, 'asset': self.serializer.asset, - 'account_username': self.serializer.username, + 'user': self.serializer.user, + 'asset': self.serializer.asset, + 'account_username': self.serializer.account_username, 'action': LoginAssetACL.ActionChoices.login_confirm } - with tmp_to_org(self.serializer.org): + with tmp_to_org(self.serializer.asset.org): acl = LoginAssetACL.filter(**queries).valid().first() - if not acl: - is_need_confirm = False - response_data = {} - else: - is_need_confirm = True + if acl: + need_confirm = True response_data = self._get_response_data_of_need_confirm(acl) - response_data['need_confirm'] = is_need_confirm - return is_need_confirm, response_data + else: + need_confirm = False + response_data = {} + response_data['need_confirm'] = need_confirm + return response_data - def _get_response_data_of_need_confirm(self, acl): + def _get_response_data_of_need_confirm(self, acl) -> dict: ticket = LoginAssetACL.create_login_asset_confirm_ticket( user=self.serializer.user, asset=self.serializer.asset, - account_username=self.serializer.username, + account_username=self.serializer.account_username, assignees=acl.reviewers.all(), - org_id=self.serializer.org.id, + org_id=self.serializer.asset.org.id, ) confirm_status_url = reverse( view_name='api-tickets:super-ticket-status', @@ -68,10 +75,3 @@ class LoginAssetCheckAPI(CreateAPIView): 'ticket_id': str(ticket.id) } return data - - @lazyproperty - def serializer(self): - serializer = self.get_serializer(data=self.request.data) - serializer.is_valid(raise_exception=True) - return serializer - diff --git a/apps/acls/serializers/login_asset_check.py b/apps/acls/serializers/login_asset_check.py index 49afda63a..eac7481d5 100644 --- a/apps/acls/serializers/login_asset_check.py +++ b/apps/acls/serializers/login_asset_check.py @@ -16,34 +16,20 @@ class LoginAssetCheckSerializer(serializers.Serializer): super().__init__(*args, **kwargs) self.user = None self.asset = None - self.username = None def validate_user_id(self, user_id): - self.user = self.validate_object_exist(User, user_id) + self.user = self.get_object(User, user_id) return user_id def validate_asset_id(self, asset_id): - self.asset = self.validate_object_exist(Asset, asset_id) + self.asset = self.get_object(Asset, asset_id) return asset_id @staticmethod - def validate_object_exist(model, field_id): + def get_object(model, pk): with tmp_to_root_org(): - obj = get_object_or_none(model, pk=field_id) - if not obj: - error = '{} Model object does not exist'.format(model.__name__) - raise serializers.ValidationError(error) - return obj - - def validate_account_username(self, account_username): - asset_id = self.initial_data.get('asset_id') - account = Account.objects.filter(username=account_username, asset_id=asset_id).first() - if not account: - error = 'Account username does not exist' - raise serializers.ValidationError(error) - self.username = account_username - return account_username - - @lazyproperty - def org(self): - return self.asset.org + obj = get_object_or_none(model, pk=pk) + if obj: + return obj + error = '{} Model object does not exist'.format(model.__name__) + raise serializers.ValidationError(error) From 4083df07cc12379963161e7f71a313d7dfa44546 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Wed, 30 Nov 2022 18:13:15 +0800 Subject: [PATCH 50/65] perf: audit navigation (#9133) Co-authored-by: feng <1304903146@qq.com> --- apps/jumpserver/api.py | 203 +++++++++++++++++++++++++++++++++++++---- apps/orgs/caches.py | 13 --- 2 files changed, 185 insertions(+), 31 deletions(-) diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py index 48b767299..aa1c85c04 100644 --- a/apps/jumpserver/api.py +++ b/apps/jumpserver/api.py @@ -13,48 +13,121 @@ from rest_framework.response import Response from users.models import User from assets.models import Asset from assets.const import AllTypes -from terminal.models import Session +from terminal.models import Session, Command from terminal.utils import ComponentsPrometheusMetricsUtil from orgs.utils import current_org from common.utils import lazyproperty +from audits.models import UserLoginLog, PasswordChangeLog, OperateLog +from audits.const import LoginStatusChoices from common.utils.timezone import local_now, local_zero_hour from orgs.caches import OrgResourceStatisticsCache __all__ = ['IndexApi'] -class DatesLoginMetricMixin: +class DateTimeMixin: request: Request + @property + def org(self): + return current_org + @lazyproperty def days(self): query_params = self.request.query_params - # monthly count = query_params.get('days') count = int(count) if count else 0 return count - @lazyproperty - def sessions_queryset(self): + @property + def days_to_datetime(self): days = self.days if days == 0: t = local_zero_hour() else: t = local_now() - timezone.timedelta(days=days) - sessions_queryset = Session.objects.filter(date_start__gte=t) - return sessions_queryset + return t @lazyproperty - def session_dates_list(self): + def dates_list(self): now = local_now() dates = [(now - timezone.timedelta(days=i)).date() for i in range(self.days)] dates.reverse() return dates def get_dates_metrics_date(self): - dates_metrics_date = [d.strftime('%m-%d') for d in self.session_dates_list] or ['0'] + dates_metrics_date = [d.strftime('%m-%d') for d in self.dates_list] or ['0'] return dates_metrics_date + @lazyproperty + def users(self): + return self.org.get_members() + + @lazyproperty + def sessions_queryset(self): + t = self.days_to_datetime + sessions_queryset = Session.objects.filter(date_start__gte=t) + return sessions_queryset + + def get_logs_queryset(self, queryset, query_params): + query = {} + if not self.org.is_root(): + if query_params == 'username': + query = { + f'{query_params}__in': self.users.values_list('username', flat=True) + } + else: + query = { + f'{query_params}__in': [str(user) for user in self.users] + } + queryset = queryset.filter(**query) + return queryset + + @lazyproperty + def login_logs_queryset(self): + t = self.days_to_datetime + queryset = UserLoginLog.objects.filter(datetime__gte=t) + queryset = self.get_logs_queryset(queryset, 'username') + return queryset + + @lazyproperty + def password_change_logs_queryset(self): + t = self.days_to_datetime + queryset = PasswordChangeLog.objects.filter(datetime__gte=t) + queryset = self.get_logs_queryset(queryset, 'user') + return queryset + + @lazyproperty + def operate_logs_queryset(self): + t = self.days_to_datetime + queryset = OperateLog.objects.filter(datetime__gte=t) + queryset = self.get_logs_queryset(queryset, 'user') + return queryset + + @lazyproperty + def ftp_logs_queryset(self): + t = self.days_to_datetime + queryset = OperateLog.objects.filter(datetime__gte=t) + queryset = self.get_logs_queryset(queryset, 'user') + return queryset + + @lazyproperty + def command_queryset(self): + t = self.days_to_datetime + t = t.timestamp() + queryset = Command.objects.filter(timestamp__gte=t) + return queryset + + +class DatesLoginMetricMixin: + dates_list: list + command_queryset: Command.objects + sessions_queryset: Session.objects + ftp_logs_queryset: OperateLog.objects + login_logs_queryset: UserLoginLog.objects + operate_logs_queryset: OperateLog.objects + password_change_logs_queryset: PasswordChangeLog.objects + @staticmethod def get_cache_key(date, tp): date_str = date.strftime("%Y%m%d") @@ -93,7 +166,7 @@ class DatesLoginMetricMixin: def get_dates_metrics_total_count_login(self): data = [] - for d in self.session_dates_list: + for d in self.dates_list: count = self.get_date_login_count(d) data.append(count) if len(data) == 0: @@ -112,7 +185,7 @@ class DatesLoginMetricMixin: def get_dates_metrics_total_count_active_users(self): data = [] - for d in self.session_dates_list: + for d in self.dates_list: count = self.get_date_user_count(d) data.append(count) return data @@ -129,11 +202,28 @@ class DatesLoginMetricMixin: def get_dates_metrics_total_count_active_assets(self): data = [] - for d in self.session_dates_list: + for d in self.dates_list: count = self.get_date_asset_count(d) data.append(count) return data + def get_date_session_count(self, date): + tp = "SESSION" + count = self.__get_data_from_cache(date, tp) + if count is not None: + return count + ds, de = self.get_date_start_2_end(date) + count = Session.objects.filter(date_start__range=(ds, de)).count() + self.__set_data_to_cache(date, tp, count) + return count + + def get_dates_metrics_total_count_sessions(self): + data = [] + for d in self.dates_list: + count = self.get_date_session_count(d) + data.append(count) + return data + @lazyproperty def get_type_to_assets(self): result = Asset.objects.annotate(type=F('platform__type')). \ @@ -181,8 +271,44 @@ class DatesLoginMetricMixin: ] return sessions + @lazyproperty + def user_login_logs_amount(self): + return self.login_logs_queryset.count() -class IndexApi(DatesLoginMetricMixin, APIView): + @lazyproperty + def user_login_success_logs_amount(self): + return self.login_logs_queryset.filter(status=LoginStatusChoices.success).count() + + @lazyproperty + def user_login_amount(self): + return self.login_logs_queryset.values('username').distinct().count() + + @lazyproperty + def operate_logs_amount(self): + return self.operate_logs_queryset.count() + + @lazyproperty + def change_password_logs_amount(self): + return self.password_change_logs_queryset.count() + + @lazyproperty + def commands_amount(self): + return self.command_queryset.count() + + @lazyproperty + def commands_danger_amount(self): + return self.command_queryset.filter(risk_level=Command.RISK_LEVEL_DANGEROUS).count() + + @lazyproperty + def sessions_amount(self): + return self.sessions_queryset.count() + + @lazyproperty + def ftp_logs_amount(self): + return self.ftp_logs_queryset.count() + + +class IndexApi(DateTimeMixin, DatesLoginMetricMixin, APIView): http_method_names = ['get'] def check_permissions(self, request): @@ -193,7 +319,7 @@ class IndexApi(DatesLoginMetricMixin, APIView): query_params = self.request.query_params - caches = OrgResourceStatisticsCache(current_org) + caches = OrgResourceStatisticsCache(self.org) _all = query_params.get('all') @@ -217,9 +343,9 @@ class IndexApi(DatesLoginMetricMixin, APIView): 'total_count_assets_this_week': caches.new_assets_amount_this_week, }) - if _all or query_params.get('total_count') or query_params.get('total_count_today_login_users'): + if _all or query_params.get('total_count') or query_params.get('total_count_login_users'): data.update({ - 'total_count_today_login_users': caches.total_count_today_login_users, + 'total_count_login_users': self.user_login_amount }) if _all or query_params.get('total_count') or query_params.get('total_count_today_active_assets'): @@ -242,9 +368,50 @@ class IndexApi(DatesLoginMetricMixin, APIView): 'total_count_today_failed_sessions': caches.total_count_today_failed_sessions, }) - if _all or query_params.get('total_count') or query_params.get('total_count_type_to_assets_amount'): + if _all or query_params.get('total_count') or query_params.get('total_count_user_login_logs'): data.update({ - 'total_count_type_to_assets_amount': self.get_type_to_assets, + 'total_count_user_login_logs': self.user_login_logs_amount, + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_user_login_success_logs'): + data.update({ + 'total_count_user_login_success_logs': self.user_login_success_logs_amount, + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_operate_logs'): + data.update({ + 'total_count_operate_logs': self.operate_logs_amount, + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_change_password_logs'): + data.update({ + 'total_count_change_password_logs': self.change_password_logs_amount, + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_commands'): + data.update({ + 'total_count_commands': self.commands_amount, + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_commands_danger'): + data.update({ + 'total_count_commands_danger': self.commands_danger_amount, + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_history_sessions'): + data.update({ + 'total_count_history_sessions': self.sessions_amount - caches.total_count_online_sessions, + }) + + if _all or query_params.get('total_count') or query_params.get('total_count_ftp_logs'): + data.update({ + 'total_count_ftp_logs': self.ftp_logs_amount, + }) + + if _all or query_params.get('session_dates_metrics'): + data.update({ + 'dates_metrics_date': self.get_dates_metrics_date(), + 'dates_metrics_total_count_session': self.get_dates_metrics_total_count_sessions(), }) if _all or query_params.get('dates_metrics'): diff --git a/apps/orgs/caches.py b/apps/orgs/caches.py index 0a665c130..cd67984a3 100644 --- a/apps/orgs/caches.py +++ b/apps/orgs/caches.py @@ -7,8 +7,6 @@ from common.cache import Cache, IntegerField from common.utils import get_logger from common.utils.timezone import local_zero_hour, local_monday from users.models import UserGroup, User -from audits.models import UserLoginLog -from audits.const import LoginStatusChoices from assets.models import Node, Domain, Asset, Account from terminal.models import Session from perms.models import AssetPermission @@ -64,7 +62,6 @@ class OrgResourceStatisticsCache(OrgRelatedCache): asset_perms_amount = IntegerField(queryset=AssetPermission.objects) total_count_online_users = IntegerField() total_count_online_sessions = IntegerField() - total_count_today_login_users = IntegerField() total_count_today_active_assets = IntegerField() total_count_today_failed_sessions = IntegerField() @@ -113,16 +110,6 @@ class OrgResourceStatisticsCache(OrgRelatedCache): def compute_total_count_online_sessions(): return Session.objects.filter(is_finished=False).count() - def compute_total_count_today_login_users(self): - t = local_zero_hour() - user_login_logs = UserLoginLog.objects.filter( - datetime__gte=t, status=LoginStatusChoices.success - ) - if not self.org.is_root(): - usernames = self.org.get_members().values('username') - user_login_logs = user_login_logs.filter(username__in=usernames) - return user_login_logs.values('username').distinct().count() - @staticmethod def compute_total_count_today_active_assets(): t = local_zero_hour() From 142348b05527b97dbe45233bff8166bba621f925 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 30 Nov 2022 17:52:00 +0800 Subject: [PATCH 51/65] =?UTF-8?q?perf:=20=E5=A2=9E=E5=8A=A0=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=E8=BF=87=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/perms/api/user_permission/assets/mixin.py | 3 +++ apps/perms/serializers/user_permission.py | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/perms/api/user_permission/assets/mixin.py b/apps/perms/api/user_permission/assets/mixin.py index e95ba7aa1..09abdf686 100644 --- a/apps/perms/api/user_permission/assets/mixin.py +++ b/apps/perms/api/user_permission/assets/mixin.py @@ -88,6 +88,9 @@ class AssetsSerializerFormatMixin: serializer_class = serializers.AssetGrantedSerializer filterset_fields = ['name', 'address', 'id', 'comment'] search_fields = ['name', 'address', 'comment'] + filterset_class = AssetFilterSet + ordering_fields = ("name", "address") + ordering = ('name',) class AssetsTreeFormatMixin(SerializeToTreeNodeMixin): diff --git a/apps/perms/serializers/user_permission.py b/apps/perms/serializers/user_permission.py index 3b60d25bb..9dcb04ae7 100644 --- a/apps/perms/serializers/user_permission.py +++ b/apps/perms/serializers/user_permission.py @@ -5,8 +5,8 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from assets.const import Category, AllTypes -from assets.serializers.asset.common import AssetProtocolsSerializer from assets.models import Node, Asset, Platform, Account +from assets.serializers.asset.common import AssetProtocolsSerializer from common.drf.fields import ObjectRelatedField, LabeledChoiceField from perms.serializers.permission import ActionChoicesField @@ -48,5 +48,6 @@ class AccountsPermedSerializer(serializers.ModelSerializer): class Meta: model = Account - fields = ['id', 'name', 'has_username', 'username', 'has_secret', 'secret_type', 'actions'] + fields = ['id', 'name', 'has_username', 'username', + 'has_secret', 'secret_type', 'actions'] read_only_fields = fields From edae6942ac8e382c864fee244e057f502eb60fe2 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Wed, 30 Nov 2022 20:02:13 +0800 Subject: [PATCH 52/65] perf: connection token client add protocol (#9134) Co-authored-by: feng <1304903146@qq.com> --- apps/authentication/api/connection_token.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 6293c4627..e9df9d33b 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -154,6 +154,7 @@ class RDPFileClientProtocolURLMixin: data = { 'id': str(token.id), 'value': token.value, + 'protocol': token.protocol, 'command': '', 'file': {} } From 314b63cec8ef76b20f7caf80aa2a17b4d98d7d88 Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Wed, 30 Nov 2022 20:21:23 +0800 Subject: [PATCH 53/65] perf: connection token launch command --- apps/terminal/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/terminal/const.py b/apps/terminal/const.py index 5177f1372..5ae73ca0f 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -133,8 +133,8 @@ class NativeClient(TextChoices): def get_launch_command(cls, name, token, endpoint, os='windows'): commands = { cls.ssh: f'ssh {token.id}@{endpoint.host} -p {endpoint.ssh_port}', - cls.putty: f'putty -ssh {token.id}@{endpoint.host} -P {endpoint.ssh_port}', - cls.xshell: f'xshell -url ssh://{token.id}:{token.value}@{endpoint.host}:{endpoint.ssh_port}', + cls.putty: f'putty.exe -ssh {token.id}@{endpoint.host} -P {endpoint.ssh_port}', + cls.xshell: f'xshell.exe -url ssh://{token.id}:{token.value}@{endpoint.host}:{endpoint.ssh_port}', # cls.mysql: 'mysql -h {hostname} -P {port} -u {username} -p', # cls.psql: { # 'default': 'psql -h {hostname} -p {port} -U {username} -W', From a0df39ad28afa9cb69da8f702c79caff925f71bb Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Wed, 30 Nov 2022 20:38:22 +0800 Subject: [PATCH 54/65] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9connection=20tok?= =?UTF-8?q?en=20=E5=AF=B9=E6=8E=A5client=E7=9A=84username?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/terminal/const.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/terminal/const.py b/apps/terminal/const.py index 5ae73ca0f..6d7a14f9d 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -131,10 +131,11 @@ class NativeClient(TextChoices): @classmethod def get_launch_command(cls, name, token, endpoint, os='windows'): + username = f'JMS-{token.id}' commands = { - cls.ssh: f'ssh {token.id}@{endpoint.host} -p {endpoint.ssh_port}', - cls.putty: f'putty.exe -ssh {token.id}@{endpoint.host} -P {endpoint.ssh_port}', - cls.xshell: f'xshell.exe -url ssh://{token.id}:{token.value}@{endpoint.host}:{endpoint.ssh_port}', + cls.ssh: f'ssh {username}@{endpoint.host} -p {endpoint.ssh_port}', + cls.putty: f'putty.exe -ssh {username}@{endpoint.host} -P {endpoint.ssh_port}', + cls.xshell: f'xshell.exe -url ssh://{username}:{token.value}@{endpoint.host}:{endpoint.ssh_port}', # cls.mysql: 'mysql -h {hostname} -P {port} -u {username} -p', # cls.psql: { # 'default': 'psql -h {hostname} -p {port} -U {username} -W', From 2aa1d664a6d8df659361bb5034aa72ba4c4f2153 Mon Sep 17 00:00:00 2001 From: Bai Date: Wed, 30 Nov 2022 21:13:50 +0800 Subject: [PATCH 55/65] =?UTF-8?q?perf:=20=E5=A4=84=E7=90=86=20acl=20filter?= =?UTF-8?q?=20=E9=80=BB=E8=BE=91=E6=94=BE=E5=88=B0=20queryset=20=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/acls/api/login_asset_check.py | 15 ++++--- apps/acls/models/login_asset_acl.py | 64 ++++++++++++----------------- apps/common/utils/ip/utils.py | 2 +- 3 files changed, 35 insertions(+), 46 deletions(-) diff --git a/apps/acls/api/login_asset_check.py b/apps/acls/api/login_asset_check.py index 662befafd..df7288c4c 100644 --- a/apps/acls/api/login_asset_check.py +++ b/apps/acls/api/login_asset_check.py @@ -30,15 +30,14 @@ class LoginAssetCheckAPI(CreateAPIView): return serializer def check_confirm(self): - queries = { - 'user': self.serializer.user, - 'asset': self.serializer.asset, - 'account_username': self.serializer.account_username, - 'action': LoginAssetACL.ActionChoices.login_confirm - } with tmp_to_org(self.serializer.asset.org): - acl = LoginAssetACL.filter(**queries).valid().first() - + acl = LoginAssetACL.objects\ + .filter(action=LoginAssetACL.ActionChoices.login_confirm)\ + .filter_user(self.serializer.user)\ + .filter_asset(self.serializer.asset)\ + .filter_account(self.serializer.account_username)\ + .valid()\ + .first() if acl: need_confirm = True response_data = self._get_response_data_of_need_confirm(acl) diff --git a/apps/acls/models/login_asset_acl.py b/apps/acls/models/login_asset_acl.py index a27a730d0..842d41432 100644 --- a/apps/acls/models/login_asset_acl.py +++ b/apps/acls/models/login_asset_acl.py @@ -6,6 +6,32 @@ from .base import BaseACL, BaseACLQuerySet from common.utils.ip import contains_ip +class ACLQuerySet(BaseACLQuerySet): + def filter_user(self, user): + return self.filter( + Q(users__username_group__contains=user.username) | + Q(users__username_group__contains='*') + ) + + def filter_asset(self, asset): + queryset = self.filter( + Q(assets__name_group__contains=asset.name) | + Q(assets__name_group__contains='*') + ) + ids = [ + q.id for q in queryset + if contains_ip(asset.address, q.assets.get('address_group', [])) + ] + queryset = LoginAssetACL.objects.filter(id__in=ids) + return queryset + + def filter_account(self, account_username): + return self.filter( + Q(accounts__username_group__contains=account_username) | + Q(accounts__username_group__contains='*') + ) + + class ACLManager(OrgManager): def valid(self): @@ -32,7 +58,7 @@ class LoginAssetACL(BaseACL, OrgModelMixin): verbose_name=_("Reviewers") ) - objects = ACLManager.from_queryset(BaseACLQuerySet)() + objects = ACLManager.from_queryset(ACLQuerySet)() class Meta: unique_together = ('name', 'org_id') @@ -42,42 +68,6 @@ class LoginAssetACL(BaseACL, OrgModelMixin): def __str__(self): return self.name - @classmethod - def filter(cls, user, asset, account_username, action): - queryset = cls.objects.filter(action=action) - queryset = cls.filter_user(user, queryset) - queryset = cls.filter_asset(asset, queryset) - queryset = cls.filter_account(account_username, queryset) - return queryset - - @classmethod - def filter_user(cls, user, queryset): - queryset = queryset.filter( - Q(users__username_group__contains=user.username) | - Q(users__username_group__contains='*') - ) - return queryset - - @classmethod - def filter_asset(cls, asset, queryset): - queryset = queryset.filter( - Q(assets__name_group__contains=asset.name) | - Q(assets__name_group__contains='*') - ) - ids = [ - q.id for q in queryset if contains_ip(asset.address, q.assets.get('address_group', [])) - ] - queryset = cls.objects.filter(id__in=ids) - return queryset - - @classmethod - def filter_account(cls, account_username, queryset): - queryset = queryset.filter( - Q(accounts__username_group__contains=account_username) | - Q(accounts__username_group__contains='*') - ) - return queryset - @classmethod def create_login_asset_confirm_ticket(cls, user, asset, account_username, assignees, org_id): from tickets.const import TicketType diff --git a/apps/common/utils/ip/utils.py b/apps/common/utils/ip/utils.py index 46b4a0e46..6a6fce26b 100644 --- a/apps/common/utils/ip/utils.py +++ b/apps/common/utils/ip/utils.py @@ -66,7 +66,7 @@ def contains_ip(ip, ip_group): if in_ip_segment(ip, _ip): return True else: - # is domain name + # address / host if ip == _ip: return True From a430b0f1a9ec6ee95ba20badded4657c1003416d Mon Sep 17 00:00:00 2001 From: Bai Date: Wed, 30 Nov 2022 23:05:20 +0800 Subject: [PATCH 56/65] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=20Login=20Asse?= =?UTF-8?q?t=20ACL=20Serializer=20reviewers=20=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/acls/serializers/login_asset_acl.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/acls/serializers/login_asset_acl.py b/apps/acls/serializers/login_asset_acl.py index 053771fb4..6e3e6bc50 100644 --- a/apps/acls/serializers/login_asset_acl.py +++ b/apps/acls/serializers/login_asset_acl.py @@ -1,9 +1,11 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ +from common.drf.fields import LabeledChoiceField +from common.drf.fields import ObjectRelatedField from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.models import Organization -from common.drf.fields import LabeledChoiceField +from users.models import User from acls import models @@ -59,6 +61,9 @@ class LoginAssetACLSerializer(BulkOrgResourceModelSerializer): users = LoginAssetACLUsersSerializer() assets = LoginAssetACLAssestsSerializer() accounts = LoginAssetACLAccountsSerializer() + reviewers = ObjectRelatedField( + queryset=User.objects, many=True, required=False, label=_('Reviewers') + ) reviewers_amount = serializers.IntegerField(read_only=True, source="reviewers.count") action = LabeledChoiceField( choices=models.LoginAssetACL.ActionChoices.choices, label=_("Action") From d198dfcba9d5b358b6a66b1dfe7acf00e2c88a81 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 1 Dec 2022 00:36:02 +0800 Subject: [PATCH 57/65] perf: granted asset add specific field --- apps/perms/serializers/user_permission.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/perms/serializers/user_permission.py b/apps/perms/serializers/user_permission.py index 9dcb04ae7..a85770a50 100644 --- a/apps/perms/serializers/user_permission.py +++ b/apps/perms/serializers/user_permission.py @@ -30,7 +30,7 @@ class AssetGrantedSerializer(serializers.ModelSerializer): 'domain', 'platform', "comment", "org_id", "is_active", ] - fields = only_fields + ['protocols', 'category', 'type'] + ['org_name'] + fields = only_fields + ['protocols', 'category', 'type', 'specific'] + ['org_name'] read_only_fields = fields From 26efc42e8b0874e1a1a0fb6fb77441a05dca0d80 Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Thu, 1 Dec 2022 10:23:04 +0800 Subject: [PATCH 58/65] perf: type to assets --- apps/jumpserver/api.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py index aa1c85c04..b0b31f4c9 100644 --- a/apps/jumpserver/api.py +++ b/apps/jumpserver/api.py @@ -408,6 +408,11 @@ class IndexApi(DateTimeMixin, DatesLoginMetricMixin, APIView): 'total_count_ftp_logs': self.ftp_logs_amount, }) + if _all or query_params.get('total_count') or query_params.get('total_count_type_to_assets_amount'): + data.update({ + 'total_count_type_to_assets_amount': self.get_type_to_assets, + }) + if _all or query_params.get('session_dates_metrics'): data.update({ 'dates_metrics_date': self.get_dates_metrics_date(), From 592d79c0f8aab18e944d50374bafea3eba81661f Mon Sep 17 00:00:00 2001 From: Bai Date: Thu, 1 Dec 2022 12:02:20 +0800 Subject: [PATCH 59/65] =?UTF-8?q?perf:=20=E5=90=88=E5=B9=B6=E6=8E=88?= =?UTF-8?q?=E6=9D=83=E8=A7=84=E5=88=99=E7=94=A8=E6=88=B7=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E7=9A=84=20API=20URL=EF=BC=8C=E7=BB=9F=E4=B8=80=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=20//=20=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/perms/api/user_permission/assets/api.py | 79 ++++++++--------- .../perms/api/user_permission/assets/mixin.py | 15 +--- apps/perms/api/user_permission/mixin.py | 23 +---- apps/perms/api/user_permission/nodes.py | 73 ++++++++-------- .../api/user_permission/nodes_with_assets.py | 32 +++---- apps/perms/urls/user_permission.py | 84 ++++++++----------- 6 files changed, 131 insertions(+), 175 deletions(-) diff --git a/apps/perms/api/user_permission/assets/api.py b/apps/perms/api/user_permission/assets/api.py index 0fc3047fd..a08644449 100644 --- a/apps/perms/api/user_permission/assets/api.py +++ b/apps/perms/api/user_permission/assets/api.py @@ -3,58 +3,58 @@ from rest_framework.generics import ListAPIView from common.utils import get_logger from .mixin import ( - UserAllGrantedAssetsQuerysetMixin, UserDirectGrantedAssetsQuerysetMixin, UserFavoriteGrantedAssetsMixin, - UserGrantedNodeAssetsMixin, AssetsSerializerFormatMixin, AssetsTreeFormatMixin, + AssetsTreeFormatMixin, + UserGrantedNodeAssetsMixin, + AssetSerializerFormatMixin, + UserFavoriteGrantedAssetsMixin, + UserAllGrantedAssetsQuerysetMixin, + UserDirectGrantedAssetsQuerysetMixin, ) -from ..mixin import AssetRoleAdminMixin, AssetRoleUserMixin +from ..mixin import SelfOrPKUserMixin, RebuildTreeMixin __all__ = [ - 'UserDirectGrantedAssetsApi', 'MyDirectGrantedAssetsApi', + 'UserDirectGrantedAssetsApi', 'UserFavoriteGrantedAssetsApi', - 'MyFavoriteGrantedAssetsApi', 'UserDirectGrantedAssetsAsTreeApi', - 'MyUngroupAssetsAsTreeApi', - 'UserAllGrantedAssetsApi', 'MyAllGrantedAssetsApi', 'MyAllAssetsAsTreeApi', + 'UserDirectGrantedAssetsAsTreeApi', + 'UserUngroupAssetsAsTreeApi', + 'UserAllGrantedAssetsApi', 'UserGrantedNodeAssetsApi', - 'MyGrantedNodeAssetsApi', ] logger = get_logger(__name__) class UserDirectGrantedAssetsApi( - AssetRoleAdminMixin, UserDirectGrantedAssetsQuerysetMixin, - AssetsSerializerFormatMixin, ListAPIView + SelfOrPKUserMixin, + UserDirectGrantedAssetsQuerysetMixin, + AssetSerializerFormatMixin, + ListAPIView ): """ 直接授权给用户的资产 """ pass -class MyDirectGrantedAssetsApi(AssetRoleUserMixin, UserDirectGrantedAssetsApi): - """ 直接授权给我的资产 """ - pass - - class UserFavoriteGrantedAssetsApi( - AssetRoleAdminMixin, UserFavoriteGrantedAssetsMixin, - AssetsSerializerFormatMixin, ListAPIView + SelfOrPKUserMixin, + UserFavoriteGrantedAssetsMixin, + AssetSerializerFormatMixin, + ListAPIView ): """ 用户收藏的授权资产 """ pass -class MyFavoriteGrantedAssetsApi(AssetRoleUserMixin, UserFavoriteGrantedAssetsApi): - """ 我收藏的授权资产 """ - pass - - -class UserDirectGrantedAssetsAsTreeApi(AssetsTreeFormatMixin, UserDirectGrantedAssetsApi): +class UserDirectGrantedAssetsAsTreeApi( + RebuildTreeMixin, + AssetsTreeFormatMixin, + UserDirectGrantedAssetsApi +): """ 用户直接授权的资产作为树 """ pass -class MyUngroupAssetsAsTreeApi(AssetRoleUserMixin, UserDirectGrantedAssetsAsTreeApi): - """ 我的未分组节点下的资产作为树 """ - +class UserUngroupAssetsAsTreeApi(UserDirectGrantedAssetsAsTreeApi): + """ 用户未分组节点下的资产作为树 """ def get_queryset(self): queryset = super().get_queryset() if not settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE: @@ -63,31 +63,20 @@ class MyUngroupAssetsAsTreeApi(AssetRoleUserMixin, UserDirectGrantedAssetsAsTree class UserAllGrantedAssetsApi( - AssetRoleAdminMixin, UserAllGrantedAssetsQuerysetMixin, - AssetsSerializerFormatMixin, ListAPIView + SelfOrPKUserMixin, + UserAllGrantedAssetsQuerysetMixin, + AssetSerializerFormatMixin, + ListAPIView ): """ 授权给用户的所有资产 """ pass -class MyAllGrantedAssetsApi(AssetRoleUserMixin, UserAllGrantedAssetsApi): - """ 授权给我的所有资产 """ - pass - - -class MyAllAssetsAsTreeApi(AssetsTreeFormatMixin, MyAllGrantedAssetsApi): - """ 授权给我的所有资产作为树 """ - pass - - class UserGrantedNodeAssetsApi( - AssetRoleAdminMixin, UserGrantedNodeAssetsMixin, - AssetsSerializerFormatMixin, ListAPIView + SelfOrPKUserMixin, + UserGrantedNodeAssetsMixin, + AssetSerializerFormatMixin, + ListAPIView ): """ 授权给用户的节点资产 """ pass - - -class MyGrantedNodeAssetsApi(AssetRoleUserMixin, UserGrantedNodeAssetsApi): - """ 授权给我的节点资产 """ - pass diff --git a/apps/perms/api/user_permission/assets/mixin.py b/apps/perms/api/user_permission/assets/mixin.py index 09abdf686..58832cdee 100644 --- a/apps/perms/api/user_permission/assets/mixin.py +++ b/apps/perms/api/user_permission/assets/mixin.py @@ -1,20 +1,18 @@ from rest_framework.request import Request from rest_framework.response import Response +from common.utils import get_logger +from users.models import User from assets.api.asset.asset import AssetFilterSet from assets.api.mixin import SerializeToTreeNodeMixin from assets.models import Asset, Node -from common.utils import get_logger from perms import serializers from perms.pagination import NodeGrantedAssetPagination, AllGrantedAssetPagination from perms.utils.user_permission import UserGrantedAssetsQueryUtils -from users.models import User logger = get_logger(__name__) -# 获取数据的 ------------------------------------------------------------ - class UserDirectGrantedAssetsQuerysetMixin: only_fields = serializers.AssetGrantedSerializer.Meta.only_fields user: User @@ -73,18 +71,13 @@ class UserGrantedNodeAssetsMixin: return Asset.objects.none() node_id = self.kwargs.get("node_id") - node, assets = UserGrantedAssetsQueryUtils(self.user).get_node_all_assets( - node_id - ) + node, assets = UserGrantedAssetsQueryUtils(self.user).get_node_all_assets(node_id) assets = assets.prefetch_related('platform').only(*self.only_fields) self.pagination_node = node return assets -# 控制格式的 ---------------------------------------------------- - - -class AssetsSerializerFormatMixin: +class AssetSerializerFormatMixin: serializer_class = serializers.AssetGrantedSerializer filterset_fields = ['name', 'address', 'id', 'comment'] search_fields = ['name', 'address', 'comment'] diff --git a/apps/perms/api/user_permission/mixin.py b/apps/perms/api/user_permission/mixin.py index 9ff8ed0f1..2c145221a 100644 --- a/apps/perms/api/user_permission/mixin.py +++ b/apps/perms/api/user_permission/mixin.py @@ -7,7 +7,6 @@ from django.utils.translation import ugettext_lazy as _ from common.http import is_true from common.utils import is_uuid from common.exceptions import JMSObjectDoesNotExist -from common.mixins.api import RoleAdminMixin, RoleUserMixin from perms.utils.user_permission import UserGrantedTreeRefreshController from rbac.permissions import RBACPermission from users.models import User @@ -23,24 +22,6 @@ class RebuildTreeMixin: return super().get(request, *args, **kwargs) -class AssetRoleAdminMixin(RebuildTreeMixin, RoleAdminMixin): - rbac_perms = ( - ('list', 'perms.view_userassets'), - ('retrieve', 'perms.view_userassets'), - ('get_tree', 'perms.view_userassets'), - ('GET', 'perms.view_userassets'), - ) - - -class AssetRoleUserMixin(RebuildTreeMixin, RoleUserMixin): - rbac_perms = ( - ('list', 'perms.view_myassets'), - ('retrieve', 'perms.view_myassets'), - ('get_tree', 'perms.view_myassets'), - ('GET', 'perms.view_myassets'), - ) - - class SelfOrPKUserMixin: kwargs: dict request: Request @@ -59,6 +40,7 @@ class SelfOrPKUserMixin: ('retrieve', 'perms.view_myassets'), ('get_tree', 'perms.view_myassets'), ('GET', 'perms.view_myassets'), + ('OPTIONS', 'perms.view_myassets'), ) @property @@ -68,6 +50,7 @@ class SelfOrPKUserMixin: ('retrieve', 'perms.view_userassets'), ('get_tree', 'perms.view_userassets'), ('GET', 'perms.view_userassets'), + ('OPTIONS', 'perms.view_userassets'), ) @property @@ -76,6 +59,8 @@ class SelfOrPKUserMixin: user = self.request.user elif is_uuid(self.kwargs.get('user')): user = get_object_or_404(User, pk=self.kwargs.get('user')) + elif hasattr(self, 'swagger_fake_view'): + user = self.request.user else: raise JMSObjectDoesNotExist(object_name=_('User')) return user diff --git a/apps/perms/api/user_permission/nodes.py b/apps/perms/api/user_permission/nodes.py index 74f0b6728..eb0d321d5 100644 --- a/apps/perms/api/user_permission/nodes.py +++ b/apps/perms/api/user_permission/nodes.py @@ -1,31 +1,25 @@ # -*- coding: utf-8 -*- # import abc -from rest_framework.generics import ( - ListAPIView -) -from rest_framework.response import Response from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.generics import ListAPIView -from assets.api.mixin import SerializeToTreeNodeMixin from common.utils import get_logger -from .mixin import AssetRoleAdminMixin, AssetRoleUserMixin -from perms.hands import User +from assets.api.mixin import SerializeToTreeNodeMixin from perms import serializers - +from perms.hands import User from perms.utils.user_permission import UserGrantedNodesQueryUtils +from .mixin import SelfOrPKUserMixin, RebuildTreeMixin logger = get_logger(__name__) __all__ = [ 'UserGrantedNodesApi', - 'MyGrantedNodesApi', - 'MyGrantedNodesAsTreeApi', - 'UserGrantedNodeChildrenForAdminApi', - 'MyGrantedNodeChildrenApi', - 'UserGrantedNodeChildrenAsTreeForAdminApi', - 'MyGrantedNodeChildrenAsTreeApi', + 'UserGrantedNodesAsTreeApi', + 'UserGrantedNodeChildrenApi', + 'UserGrantedNodeChildrenAsTreeApi', 'BaseGrantedNodeAsTreeApi', 'UserGrantedNodesMixin', ] @@ -98,35 +92,42 @@ class UserGrantedNodesMixin: return nodes -# ------------------------------------------ -# 最终的 api -class UserGrantedNodeChildrenForAdminApi(AssetRoleAdminMixin, UserGrantedNodeChildrenMixin, BaseNodeChildrenApi): +# API + + +class UserGrantedNodeChildrenApi( + SelfOrPKUserMixin, + UserGrantedNodeChildrenMixin, + BaseNodeChildrenApi +): + """ 用户授权的节点下的子节点""" pass -class MyGrantedNodeChildrenApi(AssetRoleUserMixin, UserGrantedNodeChildrenMixin, BaseNodeChildrenApi): +class UserGrantedNodeChildrenAsTreeApi( + SelfOrPKUserMixin, + RebuildTreeMixin, + UserGrantedNodeChildrenMixin, + BaseNodeChildrenAsTreeApi +): + """ 用户授权的节点下的子节点树""" pass -class UserGrantedNodeChildrenAsTreeForAdminApi(AssetRoleAdminMixin, UserGrantedNodeChildrenMixin, BaseNodeChildrenAsTreeApi): +class UserGrantedNodesApi( + SelfOrPKUserMixin, + UserGrantedNodesMixin, + BaseGrantedNodeApi +): + """ 用户授权的节点 """ pass -class MyGrantedNodeChildrenAsTreeApi(AssetRoleUserMixin, UserGrantedNodeChildrenMixin, BaseNodeChildrenAsTreeApi): - def get_permissions(self): - permissions = super().get_permissions() - return permissions - - -class UserGrantedNodesApi(AssetRoleAdminMixin, UserGrantedNodesMixin, BaseGrantedNodeApi): +class UserGrantedNodesAsTreeApi( + SelfOrPKUserMixin, + RebuildTreeMixin, + UserGrantedNodesMixin, + BaseGrantedNodeAsTreeApi +): + """ 用户授权的节点树 """ pass - - -class MyGrantedNodesApi(AssetRoleUserMixin, UserGrantedNodesApi): - pass - - -class MyGrantedNodesAsTreeApi(AssetRoleUserMixin, UserGrantedNodesMixin, BaseGrantedNodeAsTreeApi): - pass - -# ------------------------------------------ diff --git a/apps/perms/api/user_permission/nodes_with_assets.py b/apps/perms/api/user_permission/nodes_with_assets.py index 6c848878f..20cda7e00 100644 --- a/apps/perms/api/user_permission/nodes_with_assets.py +++ b/apps/perms/api/user_permission/nodes_with_assets.py @@ -1,24 +1,25 @@ # -*- coding: utf-8 -*- # +from django.conf import settings +from django.db.models import F, Value, CharField from rest_framework.generics import ListAPIView from rest_framework.request import Request from rest_framework.response import Response -from django.db.models import F, Value, CharField -from django.conf import settings -from common.utils.common import timeit -from orgs.utils import tmp_to_root_org -from common.permissions import IsValidUser from common.utils import get_logger, get_object_or_none -from .mixin import AssetRoleUserMixin, AssetRoleAdminMixin +from common.utils.common import timeit +from common.permissions import IsValidUser + +from assets.models import Asset +from assets.api import SerializeToTreeNodeMixin +from perms.hands import Node +from perms.models import AssetPermission, PermNode from perms.utils.user_permission import ( UserGrantedTreeBuildUtils, get_user_all_asset_perm_ids, UserGrantedNodesQueryUtils, UserGrantedAssetsQueryUtils, ) -from perms.models import AssetPermission, PermNode -from assets.models import Asset -from assets.api import SerializeToTreeNodeMixin -from perms.hands import Node + +from .mixin import SelfOrPKUserMixin, RebuildTreeMixin logger = get_logger(__name__) @@ -148,9 +149,10 @@ class GrantedNodeChildrenWithAssetsAsTreeApiMixin(SerializeToTreeNodeMixin, return Response(data=all_tree_nodes) -class UserGrantedNodeChildrenWithAssetsAsTreeApi(AssetRoleAdminMixin, GrantedNodeChildrenWithAssetsAsTreeApiMixin): - pass - - -class MyGrantedNodeChildrenWithAssetsAsTreeApi(AssetRoleUserMixin, GrantedNodeChildrenWithAssetsAsTreeApiMixin): +class UserGrantedNodeChildrenWithAssetsAsTreeApi( + SelfOrPKUserMixin, + RebuildTreeMixin, + GrantedNodeChildrenWithAssetsAsTreeApiMixin +): + """ 用户授权的节点的子节点与资产树 """ pass diff --git a/apps/perms/urls/user_permission.py b/apps/perms/urls/user_permission.py index c7413dbbc..973374b7a 100644 --- a/apps/perms/urls/user_permission.py +++ b/apps/perms/urls/user_permission.py @@ -3,68 +3,54 @@ from django.urls import path, include from .. import api user_permission_urlpatterns = [ - # 以 serializer 格式返回 - path('/assets/', api.UserAllGrantedAssetsApi.as_view(), name='user-assets'), - path('assets/', api.MyAllGrantedAssetsApi.as_view(), name='my-assets'), - # Tree Node 的数据格式返回 - path('/assets/tree/', api.UserDirectGrantedAssetsAsTreeApi.as_view(), name='user-assets-as-tree'), - path('assets/tree/', api.MyAllAssetsAsTreeApi.as_view(), name='my-assets-as-tree'), - path('ungroup/assets/tree/', api.MyUngroupAssetsAsTreeApi.as_view(), name='my-ungroup-assets-as-tree'), + # such as: my | self | user.id - # 获取用户所有`直接授权的节点`与`直接授权资产`关联的节点 - # 以 serializer 格式返回 - path('/nodes/', api.UserGrantedNodesApi.as_view(), name='user-nodes'), - path('nodes/', api.MyGrantedNodesApi.as_view(), name='my-nodes'), - # 以 Tree Node 的数据格式返回 - path('/nodes/tree/', api.MyGrantedNodesAsTreeApi.as_view(), name='user-nodes-as-tree'), - path('nodes/tree/', api.MyGrantedNodesAsTreeApi.as_view(), name='my-nodes-as-tree'), + # assets + path('/assets/', api.UserAllGrantedAssetsApi.as_view(), + name='user-assets'), + path('/assets/tree/', api.UserDirectGrantedAssetsAsTreeApi.as_view(), + name='user-assets-as-tree'), + path('/ungroup/assets/tree/', api.UserUngroupAssetsAsTreeApi.as_view(), + name='user-ungroup-assets-as-tree'), - # 一层一层的获取用户授权的节点, - # 以 Serializer 的数据格式返回 - path('/nodes/children/', api.UserGrantedNodeChildrenForAdminApi.as_view(), name='user-nodes-children'), - path('nodes/children/', api.MyGrantedNodeChildrenApi.as_view(), name='my-nodes-children'), - # 以 Tree Node 的数据格式返回 - path('/nodes/children/tree/', api.UserGrantedNodeChildrenAsTreeForAdminApi.as_view(), + # nodes + path('/nodes/', api.UserGrantedNodesApi.as_view(), + name='user-nodes'), + path('/nodes/tree/', api.UserGrantedNodesAsTreeApi.as_view(), + name='user-nodes-as-tree'), + path('/nodes/children/', api.UserGrantedNodeChildrenApi.as_view(), + name='user-nodes-children'), + path('/nodes/children/tree/', api.UserGrantedNodeChildrenAsTreeApi.as_view(), name='user-nodes-children-as-tree'), - # 部分调用位置 - # - 普通用户 -> 我的资产 -> 展开节点 时调用 - path('nodes/children/tree/', api.MyGrantedNodeChildrenAsTreeApi.as_view(), name='my-nodes-children-as-tree'), - # 此接口会返回整棵树 - # 普通用户 -> 命令执行 -> 左侧树 + # node-assets + path('/nodes//assets/', api.UserGrantedNodeAssetsApi.as_view(), + name='user-node-assets'), + path('/nodes/ungrouped/assets/', api.UserDirectGrantedAssetsApi.as_view(), + name='user-ungrouped-assets'), + path('/nodes/favorite/assets/', api.UserFavoriteGrantedAssetsApi.as_view(), + name='user-ungrouped-assets'), + + path('/nodes/children-with-assets/tree/', + api.UserGrantedNodeChildrenWithAssetsAsTreeApi.as_view(), + name='user-nodes-children-with-assets-as-tree'), + path('nodes-with-assets/tree/', api.MyGrantedNodesWithAssetsAsTreeApi.as_view(), name='my-nodes-with-assets-as-tree'), - # 主要用于 luna 页面,带资产的节点树 - path('/nodes/children-with-assets/tree/', api.UserGrantedNodeChildrenWithAssetsAsTreeApi.as_view(), - name='user-nodes-children-with-assets-as-tree'), - path('nodes/children-with-assets/tree/', api.MyGrantedNodeChildrenWithAssetsAsTreeApi.as_view(), - name='my-nodes-children-with-assets-as-tree'), - - # 查询授权树上某个节点的所有资产 - path('/nodes//assets/', api.UserGrantedNodeAssetsApi.as_view(), name='user-node-assets'), - path('nodes//assets/', api.MyGrantedNodeAssetsApi.as_view(), name='my-node-assets'), - - # 未分组的资产 - path('/nodes/ungrouped/assets/', api.UserDirectGrantedAssetsApi.as_view(), name='user-ungrouped-assets'), - path('nodes/ungrouped/assets/', api.MyDirectGrantedAssetsApi.as_view(), name='my-ungrouped-assets'), - - # 收藏的资产 - path('/nodes/favorite/assets/', api.UserFavoriteGrantedAssetsApi.as_view(), name='user-ungrouped-assets'), - path('nodes/favorite/assets/', api.MyFavoriteGrantedAssetsApi.as_view(), - name='my-ungrouped-assets'), - - # 获取授权给用户某个资产的所有账号 - # user params: ['my', 'self'] or user.id + # accounts path('/assets//accounts/', api.UserPermedAssetAccountsApi.as_view(), name='user-permed-asset-accounts'), ] user_group_permission_urlpatterns = [ # 查询某个用户组授权的资产和资产组 - path('/assets/', api.UserGroupGrantedAssetsApi.as_view(), name='user-group-assets'), - path('/nodes/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes'), - path('/nodes/children/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes-children'), + path('/assets/', api.UserGroupGrantedAssetsApi.as_view(), + name='user-group-assets'), + path('/nodes/', api.UserGroupGrantedNodesApi.as_view(), + name='user-group-nodes'), + path('/nodes/children/', api.UserGroupGrantedNodesApi.as_view(), + name='user-group-nodes-children'), path('/nodes/children/tree/', api.UserGroupGrantedNodeChildrenAsTreeApi.as_view(), name='user-group-nodes-children-as-tree'), path('/nodes//assets/', api.UserGroupGrantedNodeAssetsApi.as_view(), From b55b755e8e9c2951854b467e7f07b89634d764ef Mon Sep 17 00:00:00 2001 From: Bai Date: Thu, 1 Dec 2022 12:03:01 +0800 Subject: [PATCH 60/65] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20LoginAssetACL?= =?UTF-8?q?=20Check=20API=20=E8=8E=B7=E5=8F=96=20account=5Fusername=20?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/acls/api/login_asset_check.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/acls/api/login_asset_check.py b/apps/acls/api/login_asset_check.py index df7288c4c..04042473f 100644 --- a/apps/acls/api/login_asset_check.py +++ b/apps/acls/api/login_asset_check.py @@ -35,7 +35,7 @@ class LoginAssetCheckAPI(CreateAPIView): .filter(action=LoginAssetACL.ActionChoices.login_confirm)\ .filter_user(self.serializer.user)\ .filter_asset(self.serializer.asset)\ - .filter_account(self.serializer.account_username)\ + .filter_account(self.serializer.validated_data.get('account_username'))\ .valid()\ .first() if acl: @@ -51,7 +51,7 @@ class LoginAssetCheckAPI(CreateAPIView): ticket = LoginAssetACL.create_login_asset_confirm_ticket( user=self.serializer.user, asset=self.serializer.asset, - account_username=self.serializer.account_username, + account_username=self.serializer.validated_data.get('account_username'), assignees=acl.reviewers.all(), org_id=self.serializer.asset.org.id, ) From d1461b33c55130d0a9ebd691cdcd2a0c634dfcae Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Thu, 1 Dec 2022 13:12:31 +0800 Subject: [PATCH 61/65] perf: gather account mysql (#9136) Co-authored-by: feng <1304903146@qq.com> --- apps/assets/automations/gather_accounts/database/mysql/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/assets/automations/gather_accounts/database/mysql/main.yml b/apps/assets/automations/gather_accounts/database/mysql/main.yml index 4b166322a..4ba9ec221 100644 --- a/apps/assets/automations/gather_accounts/database/mysql/main.yml +++ b/apps/assets/automations/gather_accounts/database/mysql/main.yml @@ -1,7 +1,7 @@ - hosts: mysql gather_facts: no vars: - ansible_python_interpreter: /Users/xiaofeng/Desktop/jumpserver/venv/bin/python + ansible_python_interpreter: /usr/local/bin/python tasks: - name: Get info From 8162a1b17e9f2ae3ec00654e3f6329c1d09cde04 Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 1 Dec 2022 15:21:53 +0800 Subject: [PATCH 62/65] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=20gateway?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/api/domain.py | 10 +- apps/assets/migrations/0116_delete_gateway.py | 16 +++ apps/assets/migrations/0117_gateway.py | 24 ++++ apps/assets/models/asset/host.py | 8 +- apps/assets/models/domain.py | 129 +++--------------- 5 files changed, 64 insertions(+), 123 deletions(-) create mode 100644 apps/assets/migrations/0116_delete_gateway.py create mode 100644 apps/assets/migrations/0117_gateway.py diff --git a/apps/assets/api/domain.py b/apps/assets/api/domain.py index b98cd7273..948bb6a7b 100644 --- a/apps/assets/api/domain.py +++ b/apps/assets/api/domain.py @@ -35,7 +35,7 @@ class GatewayViewSet(OrgBulkModelViewSet): serializer_class = serializers.GatewaySerializer def get_queryset(self): - queryset = Host.get_gateway_queryset() + queryset = Domain.get_gateway_queryset() return queryset @@ -45,17 +45,17 @@ class GatewayTestConnectionApi(SingleObjectMixin, APIView): } def get_queryset(self): - queryset = Host.get_gateway_queryset() + queryset = Domain.get_gateway_queryset() return queryset def post(self, request, *args, **kwargs): - self.object = self.get_object() - local_port = self.request.data.get('port') or self.object.port + gateway = self.get_object() + local_port = self.request.data.get('port') or gateway.port try: local_port = int(local_port) except ValueError: raise ValidationError({'port': _('Number required')}) - ok, e = self.object.test_connective(local_port=local_port) + ok, e = gateway.test_connective(local_port=local_port) if ok: return Response("ok") else: diff --git a/apps/assets/migrations/0116_delete_gateway.py b/apps/assets/migrations/0116_delete_gateway.py new file mode 100644 index 000000000..9ccb1c1b7 --- /dev/null +++ b/apps/assets/migrations/0116_delete_gateway.py @@ -0,0 +1,16 @@ +# Generated by Django 3.2.14 on 2022-12-01 07:08 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0115_auto_20221130_1118'), + ] + + operations = [ + migrations.DeleteModel( + name='Gateway', + ), + ] diff --git a/apps/assets/migrations/0117_gateway.py b/apps/assets/migrations/0117_gateway.py new file mode 100644 index 000000000..6bf8ce138 --- /dev/null +++ b/apps/assets/migrations/0117_gateway.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.14 on 2022-12-01 07:21 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0116_delete_gateway'), + ] + + operations = [ + migrations.CreateModel( + name='Gateway', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('assets.host',), + ), + ] diff --git a/apps/assets/models/asset/host.py b/apps/assets/models/asset/host.py index a1dbb6de3..6ca93b89f 100644 --- a/apps/assets/models/asset/host.py +++ b/apps/assets/models/asset/host.py @@ -3,10 +3,4 @@ from .common import Asset class Host(Asset): - - @classmethod - def get_gateway_queryset(cls): - queryset = cls.objects.filter( - platform__name=GATEWAY_NAME - ) - return queryset + pass diff --git a/apps/assets/models/domain.py b/apps/assets/models/domain.py index 248e02f20..daa08a343 100644 --- a/apps/assets/models/domain.py +++ b/apps/assets/models/domain.py @@ -1,25 +1,20 @@ # -*- coding: utf-8 -*- # import uuid -import socket import random -import paramiko +import paramiko from django.db import models -from django.core.cache import cache -from django.db.models.query import QuerySet from django.utils.translation import ugettext_lazy as _ -from common.db import fields from common.utils import get_logger, lazyproperty from orgs.mixins.models import OrgModelMixin from assets.models import Host -from .base import BaseAccount -from ..const import SecretType +from assets.const import GATEWAY_NAME logger = get_logger(__file__) -__all__ = ['Domain', 'GatewayMixin'] +__all__ = ['Domain'] class Domain(OrgModelMixin): @@ -36,9 +31,16 @@ class Domain(OrgModelMixin): def __str__(self): return self.name + @classmethod + def get_gateway_queryset(cls): + queryset = Host.objects.filter( + platform__name=GATEWAY_NAME + ) + return queryset + @lazyproperty def gateways(self): - return Host.get_gateway_queryset().filter(domain=self, is_active=True) + return self.get_gateway_queryset().filter(domain=self, is_active=True) def select_gateway(self): return self.random_gateway() @@ -53,50 +55,11 @@ class Domain(OrgModelMixin): return random.choice(self.gateways) -class GatewayMixin: - id: uuid.UUID - port: int - address: str - accounts: QuerySet - private_key_path: str - private_key_obj: paramiko.RSAKey - UNCONNECTED_KEY_TMPL = 'asset_unconnective_gateway_{}' - UNCONNECTED_SILENCE_PERIOD_KEY_TMPL = 'asset_unconnective_gateway_silence_period_{}' - UNCONNECTED_SILENCE_PERIOD_BEGIN_VALUE = 60 * 5 - - def set_unconnected(self): - unconnected_key = self.UNCONNECTED_KEY_TMPL.format(self.id) - unconnected_silence_period_key = self.UNCONNECTED_SILENCE_PERIOD_KEY_TMPL.format(self.id) - unconnected_silence_period = cache.get( - unconnected_silence_period_key, self.UNCONNECTED_SILENCE_PERIOD_BEGIN_VALUE - ) - cache.set(unconnected_silence_period_key, unconnected_silence_period * 2) - cache.set(unconnected_key, unconnected_silence_period, unconnected_silence_period) - - def set_connective(self): - unconnected_key = self.UNCONNECTED_KEY_TMPL.format(self.id) - unconnected_silence_period_key = self.UNCONNECTED_SILENCE_PERIOD_KEY_TMPL.format(self.id) - - cache.delete(unconnected_key) - cache.delete(unconnected_silence_period_key) - - def get_is_unconnected(self): - unconnected_key = self.UNCONNECTED_KEY_TMPL.format(self.id) - return cache.get(unconnected_key, False) - - @property - def is_connective(self): - return not self.get_is_unconnected() - - @is_connective.setter - def is_connective(self, value): - if value: - self.set_connective() - else: - self.set_unconnected() +class Gateway(Host): + class Meta: + proxy = True def test_connective(self, local_port=None): - # TODO 走ansible runner if local_port is None: local_port = self.port @@ -106,7 +69,7 @@ class GatewayMixin: proxy.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: - proxy.connect(self.address, port=self.port, + proxy.connect(self.ip, port=self.port, username=self.username, password=self.password, pkey=self.private_key_obj) @@ -118,8 +81,8 @@ class GatewayMixin: socket.gaierror) as e: err = str(e) if err.startswith('[Errno None] Unable to connect to port'): - err = _('Unable to connect to port {port} on {address}') - err = err.format(port=self.port, ip=self.address) + err = _('Unable to connect to port {port} on {ip}') + err = err.format(port=self.port, ip=self.ip) elif err == 'Authentication failed.': err = _('Authentication failed') elif err == 'Connect failed': @@ -134,7 +97,7 @@ class GatewayMixin: client.connect("127.0.0.1", port=local_port, username=self.username, password=self.password, - key_filename=self.private_key_path, + key_filename=self.private_key_file, sock=sock, timeout=5) except (paramiko.SSHException, @@ -152,59 +115,3 @@ class GatewayMixin: client.close() self.is_connective = True return True, None - - @lazyproperty - def username(self): - account = self.accounts.all().first() - if account: - return account.username - logger.error(f'Gateway {self} has no account') - return '' - - def get_secret(self, secret_type): - account = self.accounts.filter(secret_type=secret_type).first() - if account: - return account.secret - logger.error(f'Gateway {self} has no {secret_type} account') - - @lazyproperty - def password(self): - secret_type = SecretType.PASSWORD - return self.get_secret(secret_type) - - @lazyproperty - def private_key(self): - secret_type = SecretType.SSH_KEY - return self.get_secret(secret_type) - - -class Gateway(BaseAccount): - class Protocol(models.TextChoices): - ssh = 'ssh', 'SSH' - - name = models.CharField(max_length=128, verbose_name='Name') - ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True) - port = models.IntegerField(default=22, verbose_name=_('Port')) - protocol = models.CharField( - choices=Protocol.choices, max_length=16, default=Protocol.ssh, verbose_name=_("Protocol") - ) - domain = models.ForeignKey(Domain, on_delete=models.CASCADE, verbose_name=_("Domain")) - comment = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Comment")) - is_active = models.BooleanField(default=True, verbose_name=_("Is active")) - password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password')) - private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key')) - public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key')) - - secret = None - secret_type = None - privileged = None - - def __str__(self): - return self.name - - class Meta: - unique_together = [('name', 'org_id')] - verbose_name = _("Gateway") - permissions = [ - ('test_gateway', _('Test gateway')) - ] From fa0382fc5ea919fdb3063eeb0f4526b260ae8b97 Mon Sep 17 00:00:00 2001 From: feng <1304903146@qq.com> Date: Thu, 1 Dec 2022 18:22:41 +0800 Subject: [PATCH 63/65] perf: gateway manager --- apps/assets/models/domain.py | 87 +++++++++---------------------- apps/assets/serializers/domain.py | 10 ---- 2 files changed, 26 insertions(+), 71 deletions(-) diff --git a/apps/assets/models/domain.py b/apps/assets/models/domain.py index daa08a343..9580b17ba 100644 --- a/apps/assets/models/domain.py +++ b/apps/assets/models/domain.py @@ -9,12 +9,13 @@ from django.utils.translation import ugettext_lazy as _ from common.utils import get_logger, lazyproperty from orgs.mixins.models import OrgModelMixin -from assets.models import Host +from assets.models import Host, Platform from assets.const import GATEWAY_NAME +from orgs.mixins.models import OrgManager logger = get_logger(__file__) -__all__ = ['Domain'] +__all__ = ['Domain', 'Gateway'] class Domain(OrgModelMixin): @@ -33,10 +34,7 @@ class Domain(OrgModelMixin): @classmethod def get_gateway_queryset(cls): - queryset = Host.objects.filter( - platform__name=GATEWAY_NAME - ) - return queryset + return Gateway.objects.all() @lazyproperty def gateways(self): @@ -55,63 +53,30 @@ class Domain(OrgModelMixin): return random.choice(self.gateways) +class GatewayManager(OrgManager): + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.filter(platform__name=GATEWAY_NAME) + return queryset + + def bulk_create(self, objs, batch_size=None, ignore_conflicts=False): + platform = Gateway().default_platform + for obj in objs: + obj.platform_id = platform.id + return super().bulk_create(objs, batch_size, ignore_conflicts) + + class Gateway(Host): + objects = GatewayManager() + class Meta: proxy = True - def test_connective(self, local_port=None): - if local_port is None: - local_port = self.port + @lazyproperty + def default_platform(self): + return Platform.objects.get(name=GATEWAY_NAME, internal=True) - client = paramiko.SSHClient() - client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - proxy = paramiko.SSHClient() - proxy.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - - try: - proxy.connect(self.ip, port=self.port, - username=self.username, - password=self.password, - pkey=self.private_key_obj) - except(paramiko.AuthenticationException, - paramiko.BadAuthenticationType, - paramiko.SSHException, - paramiko.ChannelException, - paramiko.ssh_exception.NoValidConnectionsError, - socket.gaierror) as e: - err = str(e) - if err.startswith('[Errno None] Unable to connect to port'): - err = _('Unable to connect to port {port} on {ip}') - err = err.format(port=self.port, ip=self.ip) - elif err == 'Authentication failed.': - err = _('Authentication failed') - elif err == 'Connect failed': - err = _('Connect failed') - self.is_connective = False - return False, err - - try: - sock = proxy.get_transport().open_channel( - 'direct-tcpip', ('127.0.0.1', local_port), ('127.0.0.1', 0) - ) - client.connect("127.0.0.1", port=local_port, - username=self.username, - password=self.password, - key_filename=self.private_key_file, - sock=sock, - timeout=5) - except (paramiko.SSHException, - paramiko.ssh_exception.SSHException, - paramiko.ChannelException, - paramiko.AuthenticationException, - TimeoutError) as e: - - err = getattr(e, 'text', str(e)) - if err == 'Connect failed': - err = _('Connect failed') - self.is_connective = False - return False, err - finally: - client.close() - self.is_connective = True - return True, None + def save(self, *args, **kwargs): + platform = self.default_platform + self.platform_id = platform.id + return super().save(*args, **kwargs) diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py index 3e2b4889f..f17aa19ee 100644 --- a/apps/assets/serializers/domain.py +++ b/apps/assets/serializers/domain.py @@ -93,15 +93,6 @@ class GatewaySerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeria validated_data.pop('passphrase', None) return username, password, private_key - @staticmethod - def generate_default_data(): - platform = Platform.objects.get(name=GATEWAY_NAME, internal=True) - # node = Node.objects.all().order_by('date_created').first() - data = { - 'platform': platform, - } - return data - @staticmethod def create_accounts(instance, username, password, private_key): account_name = f'{instance.name}-{_("Gateway")}' @@ -135,7 +126,6 @@ class GatewaySerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeria def create(self, validated_data): auth_fields = self.clean_auth_fields(validated_data) - validated_data.update(self.generate_default_data()) instance = super().create(validated_data) self.create_accounts(instance, *auth_fields) return instance From cb3877bbdae8347bfef98c9364ce9396606ff9ba Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 1 Dec 2022 19:41:18 +0800 Subject: [PATCH 64/65] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=20acl=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=91=BD=E4=BB=A4=E8=BF=87=E6=BB=A4=20acl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/acls/api/command_acl.py | 12 ++ .../migrations/0005_auto_20221201_1846.py | 35 ++++ .../0006_commandfilteracl_commandgroup.py | 61 +++++++ apps/acls/models/__init__.py | 1 + apps/acls/models/base.py | 21 ++- apps/acls/models/command_acl.py | 162 ++++++++++++++++++ apps/acls/models/login_acl.py | 24 +-- apps/acls/models/login_asset_acl.py | 22 +-- apps/acls/serializers/command_filter.py | 0 9 files changed, 294 insertions(+), 44 deletions(-) create mode 100644 apps/acls/api/command_acl.py create mode 100644 apps/acls/migrations/0005_auto_20221201_1846.py create mode 100644 apps/acls/migrations/0006_commandfilteracl_commandgroup.py create mode 100644 apps/acls/serializers/command_filter.py diff --git a/apps/acls/api/command_acl.py b/apps/acls/api/command_acl.py new file mode 100644 index 000000000..563e0c1a2 --- /dev/null +++ b/apps/acls/api/command_acl.py @@ -0,0 +1,12 @@ +from orgs.mixins.api import OrgBulkModelViewSet +from .. import models, serializers + + +__all__ = ['CommandFilterACLViewSet'] + + +class CommandFilterACLViewSet(OrgBulkModelViewSet): + model = models.CommandFilterACL + filterset_fields = ('name', ) + search_fields = filterset_fields + serializer_class = serializers.LoginAssetACLSerializer diff --git a/apps/acls/migrations/0005_auto_20221201_1846.py b/apps/acls/migrations/0005_auto_20221201_1846.py new file mode 100644 index 000000000..b69216896 --- /dev/null +++ b/apps/acls/migrations/0005_auto_20221201_1846.py @@ -0,0 +1,35 @@ +# Generated by Django 3.2.14 on 2022-12-01 10:46 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('acls', '0004_auto_20220831_1658'), + ] + + operations = [ + migrations.AlterField( + model_name='loginacl', + name='action', + field=models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow'), ('confirm', 'Confirm')], default='reject', max_length=64, verbose_name='Action'), + ), + migrations.AlterField( + model_name='loginacl', + name='reviewers', + field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Reviewers'), + ), + migrations.AlterField( + model_name='loginassetacl', + name='action', + field=models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow'), ('confirm', 'Confirm')], default='reject', max_length=64, verbose_name='Action'), + ), + migrations.AlterField( + model_name='loginassetacl', + name='reviewers', + field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Reviewers'), + ), + ] diff --git a/apps/acls/migrations/0006_commandfilteracl_commandgroup.py b/apps/acls/migrations/0006_commandfilteracl_commandgroup.py new file mode 100644 index 000000000..05122b733 --- /dev/null +++ b/apps/acls/migrations/0006_commandfilteracl_commandgroup.py @@ -0,0 +1,61 @@ +# Generated by Django 3.2.14 on 2022-12-01 11:39 + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('acls', '0005_auto_20221201_1846'), + ] + + operations = [ + migrations.CreateModel( + name='CommandGroup', + 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)), + ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('type', models.CharField(choices=[('command', 'Command'), ('regex', 'Regex')], default='command', max_length=16, verbose_name='Type')), + ('content', models.TextField(help_text='One line one command', verbose_name='Content')), + ('ignore_case', models.BooleanField(default=True, verbose_name='Ignore case')), + ], + options={ + 'verbose_name': 'Command filter rule', + 'unique_together': {('org_id', 'name')}, + }, + ), + migrations.CreateModel( + name='CommandFilterACL', + fields=[ + ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('priority', models.IntegerField(default=50, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')), + ('action', models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow'), ('confirm', 'Confirm')], default='reject', max_length=64, verbose_name='Action')), + ('is_active', models.BooleanField(default=True, verbose_name='Active')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('users', models.JSONField(verbose_name='User')), + ('accounts', models.JSONField(verbose_name='Account')), + ('assets', models.JSONField(verbose_name='Asset')), + ('commands', models.ManyToManyField(to='acls.CommandGroup', verbose_name='Commands')), + ('reviewers', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Reviewers')), + ], + options={ + 'verbose_name': 'Command acl', + 'ordering': ('priority', '-date_updated', 'name'), + 'unique_together': {('name', 'org_id')}, + }, + ), + ] diff --git a/apps/acls/models/__init__.py b/apps/acls/models/__init__.py index 45d49c378..3c5416992 100644 --- a/apps/acls/models/__init__.py +++ b/apps/acls/models/__init__.py @@ -1,2 +1,3 @@ from .login_acl import * from .login_asset_acl import * +from .command_acl import * diff --git a/apps/acls/models/base.py b/apps/acls/models/base.py index 73ab5c59c..6bda02df8 100644 --- a/apps/acls/models/base.py +++ b/apps/acls/models/base.py @@ -4,7 +4,13 @@ from django.core.validators import MinValueValidator, MaxValueValidator from common.mixins import CommonModelMixin -__all__ = ['BaseACL', 'BaseACLQuerySet'] +__all__ = ['BaseACL', 'BaseACLQuerySet', 'ACLManager'] + + +class ActionChoices(models.TextChoices): + reject = 'reject', _('Reject') + allow = 'allow', _('Allow') + confirm = 'confirm', _('Confirm') class BaseACLQuerySet(models.QuerySet): @@ -21,6 +27,11 @@ class BaseACLQuerySet(models.QuerySet): return self.inactive() +class ACLManager(models.Manager): + def valid(self): + return self.get_queryset().valid() + + class BaseACL(CommonModelMixin): name = models.CharField(max_length=128, verbose_name=_('Name')) priority = models.IntegerField( @@ -28,8 +39,16 @@ class BaseACL(CommonModelMixin): help_text=_("1-100, the lower the value will be match first"), validators=[MinValueValidator(1), MaxValueValidator(100)] ) + action = models.CharField( + max_length=64, verbose_name=_('Action'), + choices=ActionChoices.choices, default=ActionChoices.reject + ) + reviewers = models.ManyToManyField('users.User', blank=True, verbose_name=_("Reviewers")) is_active = models.BooleanField(default=True, verbose_name=_("Active")) comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) + objects = ACLManager.from_queryset(BaseACLQuerySet)() + ActionChoices = ActionChoices + class Meta: abstract = True diff --git a/apps/acls/models/command_acl.py b/apps/acls/models/command_acl.py index e69de29bb..4fba67a3d 100644 --- a/apps/acls/models/command_acl.py +++ b/apps/acls/models/command_acl.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +# +import re + +from django.db import models +from django.db.models import Q +from django.utils.translation import ugettext_lazy as _ + +from users.models import User, UserGroup +from orgs.mixins.models import JMSOrgBaseModel +from common.utils import lazyproperty, get_logger, get_object_or_none +from orgs.mixins.models import OrgModelMixin +from .base import BaseACL + +logger = get_logger(__file__) + + +class CommandGroup(JMSOrgBaseModel): + class Type(models.TextChoices): + command = 'command', _('Command') + regex = 'regex', _('Regex') + + name = models.CharField(max_length=128, verbose_name=_("Name")) + type = models.CharField(max_length=16, default=Type.command, choices=Type.choices, verbose_name=_("Type")) + content = models.TextField(verbose_name=_("Content"), help_text=_("One line one command")) + ignore_case = models.BooleanField(default=True, verbose_name=_('Ignore case')) + + class Meta: + unique_together = [('org_id', 'name')] + verbose_name = _("Command filter rule") + + @lazyproperty + def pattern(self): + if self.type == 'command': + s = self.construct_command_regex(content=self.content) + else: + s = r'{0}'.format(self.content) + return s + + @classmethod + def construct_command_regex(cls, content): + regex = [] + content = content.replace('\r\n', '\n') + for _cmd in content.split('\n'): + cmd = re.sub(r'\s+', ' ', _cmd) + cmd = re.escape(cmd) + cmd = cmd.replace('\\ ', '\s+') + + # 有空格就不能 铆钉单词了 + if ' ' in _cmd: + regex.append(cmd) + continue + + if not cmd: + continue + + # 如果是单个字符 + if cmd[-1].isalpha(): + regex.append(r'\b{0}\b'.format(cmd)) + else: + regex.append(r'\b{0}'.format(cmd)) + s = r'{}'.format('|'.join(regex)) + return s + + @staticmethod + def compile_regex(regex, ignore_case): + args = [] + if ignore_case: + args.append(re.IGNORECASE) + try: + pattern = re.compile(regex, *args) + except Exception as e: + error = _('The generated regular expression is incorrect: {}').format(str(e)) + logger.error(error) + return False, error, None + return True, '', pattern + + def match(self, data): + succeed, error, pattern = self.compile_regex(self.pattern, self.ignore_case) + if not succeed: + return False, '' + + found = pattern.search(data) + if not found: + return False, '' + else: + return True, found.group() + + def __str__(self): + return '{} % {}'.format(self.type, self.content) + + def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id): + from tickets.const import TicketType + from tickets.models import ApplyCommandTicket + data = { + 'title': _('Command confirm') + ' ({})'.format(session.user), + 'type': TicketType.command_confirm, + 'applicant': session.user_obj, + 'apply_run_user_id': session.user_id, + 'apply_run_asset': str(session.asset), + 'apply_run_account': str(session.account), + 'apply_run_command': run_command[:4090], + 'apply_from_session_id': str(session.id), + 'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id), + 'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id), + 'org_id': org_id, + } + ticket = ApplyCommandTicket.objects.create(**data) + assignees = self.reviewers.all() + ticket.open_by_system(assignees) + return ticket + + @classmethod + def get_queryset( + cls, user_id=None, user_group_id=None, account=None, + asset_id=None, org_id=None + ): + from assets.models import Account + user_groups = [] + user = get_object_or_none(User, pk=user_id) + if user: + user_groups.extend(list(user.groups.all())) + user_group = get_object_or_none(UserGroup, pk=user_group_id) + if user_group: + org_id = user_group.org_id + user_groups.append(user_group) + + asset = get_object_or_none(Asset, pk=asset_id) + q = Q() + if user: + q |= Q(users=user) + if user_groups: + q |= Q(user_groups__in=set(user_groups)) + if account: + org_id = account.org_id + q |= Q(accounts__contains=account.username) | \ + Q(accounts__contains=Account.AliasAccount.ALL) + if asset: + org_id = asset.org_id + q |= Q(assets=asset) + if q: + cmd_filters = CommandFilter.objects.filter(q).filter(is_active=True) + if org_id: + cmd_filters = cmd_filters.filter(org_id=org_id) + rule_ids = cmd_filters.values_list('rules', flat=True) + rules = cls.objects.filter(id__in=rule_ids) + else: + rules = cls.objects.none() + return rules + + +class CommandFilterACL(OrgModelMixin, BaseACL): + # 条件 + users = models.JSONField(verbose_name=_('User')) + accounts = models.JSONField(verbose_name=_('Account')) + assets = models.JSONField(verbose_name=_('Asset')) + commands = models.ManyToManyField(CommandGroup, verbose_name=_('Commands')) + + class Meta: + unique_together = ('name', 'org_id') + ordering = ('priority', '-date_updated', 'name') + verbose_name = _('Command acl') diff --git a/apps/acls/models/login_acl.py b/apps/acls/models/login_acl.py index 71f202b15..aedb8aa9c 100644 --- a/apps/acls/models/login_acl.py +++ b/apps/acls/models/login_acl.py @@ -1,24 +1,14 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ -from .base import BaseACL, BaseACLQuerySet + from common.utils import get_request_ip, get_ip_city from common.utils.ip import contains_ip from common.utils.time_period import contains_time_period from common.utils.timezone import local_now_display - - -class ACLManager(models.Manager): - - def valid(self): - return self.get_queryset().valid() +from .base import BaseACL class LoginACL(BaseACL): - class ActionChoices(models.TextChoices): - reject = 'reject', _('Reject') - allow = 'allow', _('Allow') - confirm = 'confirm', _('Login confirm') - # 用户 user = models.ForeignKey( 'users.User', on_delete=models.CASCADE, verbose_name=_('User'), @@ -26,16 +16,6 @@ class LoginACL(BaseACL): ) # 规则 rules = models.JSONField(default=dict, verbose_name=_('Rule')) - # 动作 - action = models.CharField( - max_length=64, verbose_name=_('Action'), - choices=ActionChoices.choices, default=ActionChoices.reject - ) - reviewers = models.ManyToManyField( - 'users.User', verbose_name=_("Reviewers"), - related_name="login_confirm_acls", blank=True - ) - objects = ACLManager.from_queryset(BaseACLQuerySet)() class Meta: ordering = ('priority', '-date_updated', 'name') diff --git a/apps/acls/models/login_asset_acl.py b/apps/acls/models/login_asset_acl.py index 842d41432..de9897c7b 100644 --- a/apps/acls/models/login_asset_acl.py +++ b/apps/acls/models/login_asset_acl.py @@ -2,7 +2,7 @@ from django.db import models from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from orgs.mixins.models import OrgModelMixin, OrgManager -from .base import BaseACL, BaseACLQuerySet +from .base import BaseACL, BaseACLQuerySet, ACLManager from common.utils.ip import contains_ip @@ -32,31 +32,11 @@ class ACLQuerySet(BaseACLQuerySet): ) -class ACLManager(OrgManager): - - def valid(self): - return self.get_queryset().valid() - - class LoginAssetACL(BaseACL, OrgModelMixin): - class ActionChoices(models.TextChoices): - login_confirm = 'login_confirm', _('Login confirm') - # 条件 users = models.JSONField(verbose_name=_('User')) accounts = models.JSONField(verbose_name=_('Account')) assets = models.JSONField(verbose_name=_('Asset')) - # 动作 - action = models.CharField( - max_length=64, choices=ActionChoices.choices, default=ActionChoices.login_confirm, - verbose_name=_('Action') - ) - # 动作: 附加字段 - # - login_confirm - reviewers = models.ManyToManyField( - 'users.User', related_name='review_login_asset_acls', blank=True, - verbose_name=_("Reviewers") - ) objects = ACLManager.from_queryset(ACLQuerySet)() diff --git a/apps/acls/serializers/command_filter.py b/apps/acls/serializers/command_filter.py new file mode 100644 index 000000000..e69de29bb From 10e3100d3c66f962549f1367036de10fb38df112 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 1 Dec 2022 22:09:16 +0800 Subject: [PATCH 65/65] fix: LoginAssetACL confirm action --- apps/acls/api/login_asset_check.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/acls/api/login_asset_check.py b/apps/acls/api/login_asset_check.py index 04042473f..6ebee2579 100644 --- a/apps/acls/api/login_asset_check.py +++ b/apps/acls/api/login_asset_check.py @@ -1,10 +1,10 @@ -from rest_framework.response import Response from rest_framework.generics import CreateAPIView +from rest_framework.response import Response from common.utils import reverse, lazyproperty from orgs.utils import tmp_to_org -from ..models import LoginAssetACL from .. import serializers +from ..models import LoginAssetACL __all__ = ['LoginAssetCheckAPI'] @@ -31,12 +31,12 @@ class LoginAssetCheckAPI(CreateAPIView): def check_confirm(self): with tmp_to_org(self.serializer.asset.org): - acl = LoginAssetACL.objects\ - .filter(action=LoginAssetACL.ActionChoices.login_confirm)\ - .filter_user(self.serializer.user)\ - .filter_asset(self.serializer.asset)\ - .filter_account(self.serializer.validated_data.get('account_username'))\ - .valid()\ + acl = LoginAssetACL.objects \ + .filter(action=LoginAssetACL.ActionChoices.confirm) \ + .filter_user(self.serializer.user) \ + .filter_asset(self.serializer.asset) \ + .filter_account(self.serializer.validated_data.get('account_username')) \ + .valid() \ .first() if acl: need_confirm = True