diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index b531c6560..86ddbcc2d 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -6,6 +6,7 @@ import urllib.parse from django.http import HttpResponse from django.shortcuts import get_object_or_404 +from django.utils import timezone from rest_framework import status from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied @@ -15,6 +16,7 @@ from rest_framework.response import Response from common.drf.api import JMSModelViewSet from common.http import is_true 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 @@ -257,6 +259,10 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView 'get_client_protocol_url': 'authentication.add_connectiontoken', } + 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) @@ -264,22 +270,36 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView return self.request.user def perform_create(self, serializer): - user = self.get_user(serializer) - asset = serializer.validated_data.get('asset') - account_username = serializer.validated_data.get('account_username') - self.validate_asset_permission(user, asset, account_username) - return super(ConnectionTokenViewSet, self).perform_create(serializer) + self.validate_serializer(serializer) + return super().perform_create(serializer) - @staticmethod - def validate_asset_permission(user, asset, account_username): + def validate_serializer(self, serializer): from perms.utils.account import PermAccountUtil - actions, expire_at = PermAccountUtil().validate_permission(user, asset, account_username) - if not actions: - error = 'No actions' - raise PermissionDenied(error) - if expire_at < time.time(): - error = 'Expired' - raise PermissionDenied(error) + + data = serializer.validated_data + user = self.get_user(serializer) + asset = data.get('asset') + login = data.get('login') + data['org_id'] = asset.org_id + data['user'] = user + + util = PermAccountUtil() + permed_account = util.validate_permission(user, asset, login) + + if not permed_account or not permed_account.actions: + msg = 'user `{}` not has asset `{}` permission for login `{}`'.format( + user, asset, login + ) + raise PermissionDenied(msg) + + if permed_account.date_expired < timezone.now(): + raise PermissionDenied('Expired') + + if permed_account.has_secret: + serializer.validated_data['secret'] = '' + if permed_account.username != '@INPUT': + serializer.validated_data['username'] = '' + return permed_account class SuperConnectionTokenViewSet(ConnectionTokenViewSet): diff --git a/apps/authentication/migrations/0012_auto_20220816_1629.py b/apps/authentication/migrations/0012_auto_20220816_1629.py index 8310c4fbb..07c9559ce 100644 --- a/apps/authentication/migrations/0012_auto_20220816_1629.py +++ b/apps/authentication/migrations/0012_auto_20220816_1629.py @@ -16,7 +16,7 @@ def migrate_system_user_to_account(apps, schema_editor): count += len(connection_tokens) updated = [] for connection_token in connection_tokens: - connection_token.account_username = connection_token.system_user.username + connection_token.account = connection_token.system_user.username updated.append(connection_token) connection_token_model.objects.bulk_update(updated, ['account_username']) diff --git a/apps/authentication/migrations/0014_auto_20221122_2152.py b/apps/authentication/migrations/0014_auto_20221122_2152.py new file mode 100644 index 000000000..6421b7f0b --- /dev/null +++ b/apps/authentication/migrations/0014_auto_20221122_2152.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.14 on 2022-11-22 13:52 + +import common.db.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0013_connectiontoken_protocol'), + ] + + operations = [ + migrations.RenameField( + model_name='connectiontoken', + old_name='account_username', + new_name='login' + ), + migrations.AddField( + model_name='connectiontoken', + name='username', + field=models.CharField(default='', max_length=128, verbose_name='Username'), + ), + migrations.AlterField( + model_name='connectiontoken', + name='secret', + field=common.db.fields.EncryptCharField(default='', max_length=128, verbose_name='Secret'), + ), + ] diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py index 48c61f954..dd2e3d38f 100644 --- a/apps/authentication/models/connection_token.py +++ b/apps/authentication/models/connection_token.py @@ -2,13 +2,15 @@ import time from datetime import timedelta from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from django.conf import settings -from orgs.mixins.models import OrgModelMixin - from django.db import models -from common.utils import lazyproperty +from django.conf import settings +from rest_framework.exceptions import PermissionDenied + +from orgs.mixins.models import OrgModelMixin +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 @@ -25,13 +27,14 @@ 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")) protocol = models.CharField( choices=Protocol.choices, max_length=16, default=Protocol.ssh, verbose_name=_("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")) - account_username = models.CharField(max_length=128, default='', verbose_name=_("Account")) - secret = models.CharField(max_length=64, default='', verbose_name=_("Secret")) date_expired = models.DateTimeField( default=date_expired_default, verbose_name=_("Date expired") ) @@ -59,9 +62,10 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel): seconds = 0 return int(seconds) - @classmethod - def get_default_date_expired(cls): - return date_expired_default() + def save(self, *args, **kwargs): + self.asset_display = pretty_string(self.asset, max_length=128) + self.user_display = pretty_string(self.user, max_length=128) + return super().save(*args, **kwargs) def expire(self): self.date_expired = timezone.now() @@ -69,7 +73,7 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel): def renewal(self): """ 续期 Token,将来支持用户自定义创建 token 后,续期策略要修改 """ - self.date_expired = self.get_default_date_expired() + self.date_expired = date_expired_default() self.save() # actions 和 expired_at 在 check_valid() 中赋值 @@ -89,28 +93,52 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel): is_valid = False error = _('No asset or inactive asset') return is_valid, error - if not self.account_username: + if not self.login: is_valid = False error = _('No account') return is_valid, error - actions, expire_at = PermAccountUtil().validate_permission( - self.user, self.asset, self.account_username + + permed_account = PermAccountUtil().validate_permission( + self.user, self.asset, self.login ) - if not actions or expire_at < time.time(): - is_valid = False - error = _('User has no permission to access asset or permission expired') - return is_valid, error - self.actions = actions - self.expire_at = expire_at + if not permed_account or not permed_account.actions: + msg = 'user `{}` not has asset `{}` permission for login `{}`'.format( + self.user, self.asset, self.login + ) + raise PermissionDenied(msg) + + if permed_account.date_expired < timezone.now(): + raise PermissionDenied('Expired') + is_valid, error = True, '' return is_valid, error @lazyproperty - def account(self): + def platform(self): + return self.asset.platform + + @lazyproperty + def accounts(self): if not self.asset: return None - account = self.asset.accounts.filter(username=self.account_username).first() - return account + + data = [] + if self.login == '@INPUT': + data.append({ + 'name': self.login, + 'username': self.username, + 'secret_type': 'password', + 'secret': self.secret + }) + else: + accounts = self.asset.accounts.filter(username=self.login) + for account in accounts: + data.append({ + 'username': account.uesrname, + 'secret_type': account.secret_type, + 'secret': account.secret if account.secret else self.secret + }) + return data @lazyproperty def domain(self): diff --git a/apps/authentication/serializers/connection_token.py b/apps/authentication/serializers/connection_token.py index 5011d73a6..14aae463e 100644 --- a/apps/authentication/serializers/connection_token.py +++ b/apps/authentication/serializers/connection_token.py @@ -2,9 +2,8 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from assets.models import Asset, Gateway, Domain, CommandFilterRule, Account, Platform +from assets.serializers import PlatformSerializer from authentication.models import ConnectionToken -from common.utils import pretty_string -from common.utils.random import random_string from orgs.mixins.serializers import OrgResourceModelSerializerMixin from perms.serializers.permission import ActionChoicesField from users.models import User @@ -16,6 +15,8 @@ __all__ = [ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): + username = serializers.CharField(max_length=128, label=_("Input username"), + allow_null=True, allow_blank=True) is_valid = serializers.BooleanField(read_only=True, label=_('Validity')) expire_time = serializers.IntegerField(read_only=True, label=_('Expired time')) @@ -23,9 +24,10 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): model = ConnectionToken fields_mini = ['id'] fields_small = fields_mini + [ - 'secret', 'account_username', 'date_expired', - 'date_created', 'date_updated', - 'created_by', 'updated_by', 'org_id', 'org_name', + 'protocol', 'login', 'secret', 'username', + 'date_expired', 'date_created', + 'date_updated', 'created_by', + 'updated_by', 'org_id', 'org_name', ] fields_fk = [ 'user', 'asset', @@ -45,32 +47,6 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): def get_user(self, attrs): return self.get_request_user() - def validate(self, attrs): - fields_attrs = self.construct_internal_fields_attrs(attrs) - attrs.update(fields_attrs) - return attrs - - def construct_internal_fields_attrs(self, attrs): - asset = attrs.get('asset') or '' - asset_display = pretty_string(str(asset), max_length=128) - user = self.get_user(attrs) - user_display = pretty_string(str(user), max_length=128) - secret = attrs.get('secret') or random_string(16) - date_expired = attrs.get('date_expired') or ConnectionToken.get_default_date_expired() - org_id = asset.org_id - if not isinstance(asset, Asset): - error = '' - raise serializers.ValidationError(error) - attrs = { - 'user': user, - 'secret': secret, - 'user_display': user_display, - 'asset_display': asset_display, - 'date_expired': date_expired, - 'org_id': org_id, - } - return attrs - class ConnectionTokenDisplaySerializer(ConnectionTokenSerializer): class Meta(ConnectionTokenSerializer.Meta): @@ -122,7 +98,7 @@ class ConnectionTokenAccountSerializer(serializers.ModelSerializer): class Meta: model = Account fields = [ - 'id', 'name', 'username', 'secret_type', 'secret', 'version' + 'username', 'secret_type', 'secret', ] @@ -154,26 +130,31 @@ class ConnectionTokenCmdFilterRuleSerializer(serializers.ModelSerializer): ] -class ConnectionTokenPlatform(serializers.ModelSerializer): - class Meta: +class ConnectionTokenPlatform(PlatformSerializer): + class Meta(PlatformSerializer.Meta): model = Platform - fields = ['id', 'name', 'org_id'] + + def get_field_names(self, declared_fields, info): + names = super().get_field_names(declared_fields, info) + names = [n for n in names if n not in ['automation']] + return names class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin): user = ConnectionTokenUserSerializer(read_only=True) asset = ConnectionTokenAssetSerializer(read_only=True) platform = ConnectionTokenPlatform(read_only=True) - account = ConnectionTokenAccountSerializer(read_only=True) + accounts = ConnectionTokenAccountSerializer(read_only=True, many=True) gateway = ConnectionTokenGatewaySerializer(read_only=True) - cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True) + # cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True) actions = ActionChoicesField() expire_at = serializers.IntegerField() class Meta: model = ConnectionToken fields = [ - 'id', 'secret', 'user', 'asset', 'account_username', - 'account', 'protocol', 'domain', 'gateway', - 'cmd_filter_rules', 'actions', 'expire_at', + 'id', 'secret', 'user', 'asset', 'login', + 'accounts', 'protocol', 'domain', 'gateway', + 'actions', 'expire_at', + 'platform', ] diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py index 6f1e34b0a..03338e9aa 100644 --- a/apps/common/utils/common.py +++ b/apps/common/utils/common.py @@ -344,7 +344,7 @@ def get_file_by_arch(dir, filename): return file_path -def pretty_string(data: str, max_length=128, ellipsis_str='...'): +def pretty_string(data, max_length=128, ellipsis_str='...'): """ params: data: abcdefgh @@ -353,6 +353,7 @@ def pretty_string(data: str, max_length=128, ellipsis_str='...'): return: ab...gh """ + data = str(data) if len(data) < max_length: return data remain_length = max_length - len(ellipsis_str) diff --git a/apps/common/utils/geoip/GeoLite2-City.mmdb b/apps/common/utils/geoip/GeoLite2-City.mmdb new file mode 100644 index 000000000..c3b9d8bac --- /dev/null +++ b/apps/common/utils/geoip/GeoLite2-City.mmdb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:860b4d38beff81667c64da41c026a7dd28c3c93a28ae61fefaa7c26875f35638 +size 73906864 diff --git a/apps/jumpserver/context_processor.py b/apps/jumpserver/context_processor.py index 414144d39..84c5a4bc2 100644 --- a/apps/jumpserver/context_processor.py +++ b/apps/jumpserver/context_processor.py @@ -6,7 +6,7 @@ from django.utils.translation import ugettext_lazy as _ default_interface = dict(( ('logo_logout', static('img/logo.png')), - ('logo_index', static('img/logo_text.png')), + ('logo_index', static('img/logo_text_white.png')), ('login_image', static('img/login_image.jpg')), ('favicon', static('img/facio.ico')), ('login_title', _('JumpServer Open Source Bastion Host')), diff --git a/apps/perms/api/perm_token.py b/apps/perms/api/perm_token.py new file mode 100644 index 000000000..63cf08062 --- /dev/null +++ b/apps/perms/api/perm_token.py @@ -0,0 +1,5 @@ +from rest_framework.viewsets import ModelViewSet + + +class PermTokenViewSet(ModelViewSet): + pass diff --git a/apps/perms/utils/account.py b/apps/perms/utils/account.py index a9c2bc279..b394bfbc4 100644 --- a/apps/perms/utils/account.py +++ b/apps/perms/utils/account.py @@ -17,10 +17,8 @@ class PermAccountUtil(AssetPermissionUtil): """ permed_accounts = self.get_permed_accounts_for_user(user, asset) accounts_mapper = {account.username: account for account in permed_accounts} - account = accounts_mapper.get(account_username) - actions, date_expired = (account.actions, account.date_expired) if account else (False, None) - return actions, date_expired + return account def get_permed_accounts_for_user(self, user, asset): """ 获取授权给用户某个资产的账号 """ diff --git a/apps/tickets/migrations/0024_auto_20221121_1800.py b/apps/tickets/migrations/0024_auto_20221121_1800.py new file mode 100644 index 000000000..acd203ef6 --- /dev/null +++ b/apps/tickets/migrations/0024_auto_20221121_1800.py @@ -0,0 +1,48 @@ +# Generated by Django 3.2.14 on 2022-11-21 10:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tickets', '0023_alter_applyassetticket_apply_actions'), + ] + + operations = [ + migrations.AlterField( + model_name='approvalrule', + name='strategy', + field=models.CharField(choices=[('org_admin', 'Org admin'), ('custom_user', 'Custom user'), ('super_admin', 'Super admin'), ('super_org_admin', 'Super admin and org admin')], default='super_admin', max_length=64, verbose_name='Approve strategy'), + ), + migrations.AlterField( + model_name='ticket', + name='state', + field=models.CharField(choices=[('pending', 'Open'), ('closed', 'Cancel'), ('reopen', 'Reopen'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='pending', max_length=16, verbose_name='State'), + ), + migrations.AlterField( + model_name='ticket', + name='type', + field=models.CharField(choices=[('general', 'General'), ('apply_asset', 'Apply for asset'), ('login_confirm', 'Login confirm'), ('command_confirm', 'Command confirm'), ('login_asset_confirm', 'Login asset confirm')], default='general', max_length=64, verbose_name='Type'), + ), + migrations.AlterField( + model_name='ticketassignee', + name='state', + field=models.CharField(choices=[('pending', 'Open'), ('closed', 'Cancel'), ('reopen', 'Reopen'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='pending', max_length=64), + ), + migrations.AlterField( + model_name='ticketflow', + name='type', + field=models.CharField(choices=[('general', 'General'), ('apply_asset', 'Apply for asset'), ('login_confirm', 'Login confirm'), ('command_confirm', 'Command confirm'), ('login_asset_confirm', 'Login asset confirm')], default='general', max_length=64, verbose_name='Type'), + ), + migrations.AlterField( + model_name='ticketstep', + name='state', + field=models.CharField(choices=[('pending', 'Pending'), ('closed', 'Closed'), ('reopen', 'Reopen'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='pending', max_length=64, verbose_name='State'), + ), + migrations.AlterField( + model_name='ticketstep', + name='status', + field=models.CharField(choices=[('active', 'Active'), ('closed', 'Closed'), ('pending', 'Pending')], default='pending', max_length=16), + ), + ]