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):