diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 86ddbcc2d..1636c2cb1 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -28,9 +28,6 @@ from ..serializers import ( __all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet'] -# ExtraActionApiMixin - - class RDPFileClientProtocolURLMixin: request: Request get_serializer: callable @@ -72,8 +69,7 @@ class RDPFileClientProtocolURLMixin: # 设置磁盘挂载 drives_redirect = is_true(self.request.query_params.get('drives_redirect')) if drives_redirect: - actions = ActionChoices.choices_to_value(token.actions) - if actions & Action.TRANSFER == Action.TRANSFER: + if ActionChoices.contains(token.actions, ActionChoices.transfer()): rdp_options['drivestoredirect:s'] = '*' # 设置全屏 @@ -181,22 +177,10 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin): get_serializer: callable perform_create: callable - @action(methods=['POST'], detail=False, url_path='secret-info/detail') - def get_secret_detail(self, request, *args, **kwargs): - """ 非常重要的 api, 在逻辑层再判断一下 rbac 权限, 双重保险 """ - 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 = get_object_or_404(ConnectionToken, pk=token_id) - self.check_token_permission(token) - serializer = self.get_serializer(instance=token) - return Response(serializer.data, status=status.HTTP_200_OK) - @action(methods=['POST', 'GET'], detail=False, url_path='rdp/file') def get_rdp_file(self, request, *args, **kwargs): token = self.create_connection_token() - self.check_token_permission(token) + token.is_valid() filename, content = self.get_rdp_file_info(token) filename = '{}.rdp'.format(filename) response = HttpResponse(content, content_type='application/octet-stream') @@ -206,7 +190,7 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin): @action(methods=['POST', 'GET'], detail=False, url_path='client-url') def get_client_protocol_url(self, request, *args, **kwargs): token = self.create_connection_token() - self.check_token_permission(token) + token.is_valid() try: protocol_data = self.get_client_protocol_data(token) except ValueError as e: @@ -224,12 +208,6 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin): instance.expire() return Response(status=status.HTTP_204_NO_CONTENT) - @staticmethod - def check_token_permission(token: ConnectionToken): - is_valid, error = token.check_permission() - if not is_valid: - raise PermissionDenied(error) - 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) @@ -259,6 +237,18 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView 'get_client_protocol_url': 'authentication.add_connectiontoken', } + @action(methods=['POST'], detail=False, url_path='secret') + def get_secret_detail(self, request, *args, **kwargs): + """ 非常重要的 api, 在逻辑层再判断一下 rbac 权限, 双重保险 """ + 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 = get_object_or_404(ConnectionToken, pk=token_id) + token.is_valid() + 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) @@ -296,9 +286,9 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView raise PermissionDenied('Expired') if permed_account.has_secret: - serializer.validated_data['secret'] = '' + data['secret'] = '' if permed_account.username != '@INPUT': - serializer.validated_data['username'] = '' + data['username'] = '' return permed_account diff --git a/apps/authentication/migrations/0014_auto_20221122_2152.py b/apps/authentication/migrations/0014_auto_20221122_2152.py index 6421b7f0b..b198295a5 100644 --- a/apps/authentication/migrations/0014_auto_20221122_2152.py +++ b/apps/authentication/migrations/0014_auto_20221122_2152.py @@ -16,6 +16,11 @@ class Migration(migrations.Migration): old_name='account_username', new_name='login' ), + migrations.AlterField( + model_name='connectiontoken', + name='login', + field=models.CharField(max_length=128, verbose_name='Login account'), + ), migrations.AddField( model_name='connectiontoken', name='username', diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py index dd2e3d38f..058d07581 100644 --- a/apps/authentication/models/connection_token.py +++ b/apps/authentication/models/connection_token.py @@ -46,10 +46,6 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel): ('view_connectiontokensecret', _('Can view connection token secret')) ] - @property - def is_valid(self): - return not self.is_expired - @property def is_expired(self): return self.date_expired < timezone.now() @@ -76,69 +72,71 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel): self.date_expired = date_expired_default() self.save() - # actions 和 expired_at 在 check_valid() 中赋值 - actions = expire_at = None + @lazyproperty + def permed_account(self): + from perms.utils import PermAccountUtil + permed_account = PermAccountUtil().validate_permission( + self.user, self.asset, self.login + ) + return permed_account - def check_permission(self): - from perms.utils.account import PermAccountUtil + @lazyproperty + def actions(self): + return self.permed_account.actions + + @lazyproperty + def expire_at(self): + return self.permed_account.date_expired.timestamp() + + def is_valid(self): if self.is_expired: - is_valid = False error = _('Connection token expired at: {}').format(as_current_tz(self.date_expired)) - return is_valid, error + raise PermissionDenied(error) if not self.user or not self.user.is_valid: - is_valid = False error = _('No user or invalid user') - return is_valid, error + raise PermissionDenied(error) if not self.asset or not self.asset.is_active: is_valid = False error = _('No asset or inactive asset') return is_valid, error if not self.login: - is_valid = False error = _('No account') - return is_valid, error + raise PermissionDenied(error) - permed_account = PermAccountUtil().validate_permission( - self.user, self.asset, self.login - ) - if not permed_account or not permed_account.actions: + 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 ) raise PermissionDenied(msg) - if permed_account.date_expired < timezone.now(): + if self.permed_account.date_expired < timezone.now(): raise PermissionDenied('Expired') - - is_valid, error = True, '' - return is_valid, error + return True @lazyproperty def platform(self): return self.asset.platform @lazyproperty - def accounts(self): + def account(self): if not self.asset: return None - data = [] - if self.login == '@INPUT': - data.append({ + account = self.asset.accounts.filter(name=self.login).first() + if self.login == '@INPUT' or not account: + return { '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 + return { + 'name': account.name, + 'username': account.username, + 'secret_type': account.secret_type, + 'secret': account.secret_type or self.secret + } @lazyproperty def domain(self): diff --git a/apps/authentication/serializers/connection_token.py b/apps/authentication/serializers/connection_token.py index 3acf1333b..77981cd4a 100644 --- a/apps/authentication/serializers/connection_token.py +++ b/apps/authentication/serializers/connection_token.py @@ -17,7 +17,6 @@ __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')) class Meta: @@ -25,7 +24,7 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): fields_mini = ['id'] fields_small = fields_mini + [ 'protocol', 'login', 'secret', 'username', - 'date_expired', 'date_created', + 'actions', 'date_expired', 'date_created', 'date_updated', 'created_by', 'updated_by', 'org_id', 'org_name', ] @@ -34,7 +33,7 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): ] read_only_fields = [ # 普通 Token 不支持指定 user - 'user', 'is_valid', 'expire_time', + 'user', 'expire_time', 'user_display', 'asset_display', ] fields = fields_small + fields_fk + read_only_fields @@ -98,7 +97,7 @@ class ConnectionTokenAccountSerializer(serializers.ModelSerializer): class Meta: model = Account fields = [ - 'username', 'secret_type', 'secret', + 'name', 'username', 'secret_type', 'secret', ] @@ -144,7 +143,7 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin): user = ConnectionTokenUserSerializer(read_only=True) asset = ConnectionTokenAssetSerializer(read_only=True) platform = ConnectionTokenPlatform(read_only=True) - accounts = ConnectionTokenAccountSerializer(read_only=True, many=True) + account = ConnectionTokenAccountSerializer(read_only=True) gateway = ConnectionTokenGatewaySerializer(read_only=True) # cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True) actions = ActionChoicesField() @@ -153,8 +152,7 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin): class Meta: model = ConnectionToken fields = [ - 'id', 'secret', 'user', 'asset', 'login', - 'accounts', 'protocol', 'domain', 'gateway', - 'actions', 'expire_at', - 'platform', + 'id', 'secret', 'user', 'asset', 'account', + 'protocol', 'domain', 'gateway', + 'actions', 'expire_at', 'platform', ] diff --git a/apps/ops/api/job.py b/apps/ops/api/job.py index eaad4514c..86a52f373 100644 --- a/apps/ops/api/job.py +++ b/apps/ops/api/job.py @@ -1,4 +1,3 @@ -from django.shortcuts import get_object_or_404 from rest_framework import viewsets from ops.models import Job, JobExecution @@ -7,14 +6,17 @@ from ops.serializers.job import JobSerializer, JobExecutionSerializer __all__ = ['JobViewSet', 'JobExecutionViewSet'] from ops.tasks import run_ops_job, run_ops_job_executions +from orgs.mixins.api import OrgBulkModelViewSet -class JobViewSet(viewsets.ModelViewSet): +class JobViewSet(OrgBulkModelViewSet): serializer_class = JobSerializer - queryset = Job.objects.all() + model = Job + permission_classes = () def get_queryset(self): - return self.queryset.filter(instant=False) + query_set = super().get_queryset() + return query_set.filter(instant=False) def perform_create(self, serializer): instance = serializer.save() @@ -22,20 +24,20 @@ class JobViewSet(viewsets.ModelViewSet): run_ops_job.delay(instance.id) -class JobExecutionViewSet(viewsets.ModelViewSet): +class JobExecutionViewSet(OrgBulkModelViewSet): serializer_class = JobExecutionSerializer - queryset = JobExecution.objects.all() 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) def get_queryset(self): + query_set = super().get_queryset() job_id = self.request.query_params.get('job_id') - job_type = self.request.query_params.get('type') if job_id: - self.queryset = self.queryset.filter(job_id=job_id) - if job_type: - self.queryset = self.queryset.filter(job__type=job_type) - return self.queryset + self.queryset = query_set.filter(job_id=job_id) + return query_set diff --git a/apps/ops/migrations/0034_job_org_id.py b/apps/ops/migrations/0034_job_org_id.py new file mode 100644 index 000000000..07926cec3 --- /dev/null +++ b/apps/ops/migrations/0034_job_org_id.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2022-11-23 09:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0033_auto_20221118_1431'), + ] + + operations = [ + migrations.AddField( + model_name='job', + name='org_id', + field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), + ), + ] diff --git a/apps/ops/migrations/0035_jobexecution_org_id.py b/apps/ops/migrations/0035_jobexecution_org_id.py new file mode 100644 index 000000000..1161d10e3 --- /dev/null +++ b/apps/ops/migrations/0035_jobexecution_org_id.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2022-11-23 10:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0034_job_org_id'), + ] + + operations = [ + migrations.AddField( + model_name='jobexecution', + name='org_id', + field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), + ), + ] diff --git a/apps/ops/models/job.py b/apps/ops/models/job.py index d5542970d..f2e7eaa4b 100644 --- a/apps/ops/models/job.py +++ b/apps/ops/models/job.py @@ -9,16 +9,14 @@ from django.utils.translation import gettext_lazy as _ from django.utils import timezone from celery import current_task -from common.const.choices import Trigger -from common.db.models import BaseCreateUpdateModel - __all__ = ["Job", "JobExecution"] from ops.ansible import JMSInventory, AdHocRunner, PlaybookRunner from ops.mixin import PeriodTaskModelMixin +from orgs.mixins.models import JMSOrgBaseModel -class Job(BaseCreateUpdateModel, PeriodTaskModelMixin): +class Job(JMSOrgBaseModel, PeriodTaskModelMixin): class Types(models.TextChoices): adhoc = 'adhoc', _('Adhoc') playbook = 'playbook', _('Playbook') @@ -94,7 +92,7 @@ class Job(BaseCreateUpdateModel, PeriodTaskModelMixin): return self.executions.create() -class JobExecution(BaseCreateUpdateModel): +class JobExecution(JMSOrgBaseModel): id = models.UUIDField(default=uuid.uuid4, primary_key=True) task_id = models.UUIDField(null=True) status = models.CharField(max_length=16, verbose_name=_('Status'), default='running') diff --git a/apps/ops/serializers/job.py b/apps/ops/serializers/job.py index 389b92ce2..e5d76f85b 100644 --- a/apps/ops/serializers/job.py +++ b/apps/ops/serializers/job.py @@ -1,14 +1,13 @@ -from django.db import transaction from rest_framework import serializers - from common.drf.fields import ReadableHiddenField from ops.mixin import PeriodTaskSerializerMixin from ops.models import Job, JobExecution +from orgs.mixins.serializers import BulkOrgResourceModelSerializer _all_ = [] -class JobSerializer(serializers.ModelSerializer, PeriodTaskSerializerMixin): +class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin): owner = ReadableHiddenField(default=serializers.CurrentUserDefault()) class Meta: diff --git a/apps/ops/tasks.py b/apps/ops/tasks.py index 841f759ff..51541dd32 100644 --- a/apps/ops/tasks.py +++ b/apps/ops/tasks.py @@ -10,6 +10,7 @@ from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from common.utils import get_logger, get_object_or_none, get_log_keep_day +from orgs.utils import tmp_to_org from .celery.decorator import ( register_as_period_task, after_app_shutdown_clean_periodic, after_app_ready_start @@ -27,28 +28,30 @@ logger = get_logger(__file__) @shared_task(soft_time_limit=60, queue="ansible", verbose_name=_("Run ansible task")) def run_ops_job(job_id): job = get_object_or_none(Job, id=job_id) - 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)) + 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)) @shared_task(soft_time_limit=60, queue="ansible", verbose_name=_("Run ansible task execution")) def run_ops_job_executions(execution_id, **kwargs): execution = get_object_or_none(JobExecution, id=execution_id) - 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)) + with tmp_to_org(execution.org): + 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)) @shared_task(verbose_name=_('Periodic clear celery tasks')) diff --git a/apps/perms/const.py b/apps/perms/const.py index 50141f386..690fb2742 100644 --- a/apps/perms/const.py +++ b/apps/perms/const.py @@ -28,8 +28,16 @@ class ActionChoices(BitChoices): ) @classmethod - def has_perm(cls, action_name, total): - action_value = getattr(cls, action_name) + def transfer(cls): + return cls.upload | cls.download + + @classmethod + def clipboard(cls): + return cls.copy | cls.paste + + @classmethod + def contains(cls, total, action): + action_value = getattr(cls, action) return action_value & total == action_value @classmethod