Merge remote-tracking branch 'origin/v3' into v3

pull/9118/head^2
feng 2022-11-24 10:55:43 +08:00
commit da9516608f
11 changed files with 146 additions and 109 deletions

View File

@ -28,9 +28,6 @@ from ..serializers import (
__all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet'] __all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet']
# ExtraActionApiMixin
class RDPFileClientProtocolURLMixin: class RDPFileClientProtocolURLMixin:
request: Request request: Request
get_serializer: callable get_serializer: callable
@ -72,8 +69,7 @@ class RDPFileClientProtocolURLMixin:
# 设置磁盘挂载 # 设置磁盘挂载
drives_redirect = is_true(self.request.query_params.get('drives_redirect')) drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
if drives_redirect: if drives_redirect:
actions = ActionChoices.choices_to_value(token.actions) if ActionChoices.contains(token.actions, ActionChoices.transfer()):
if actions & Action.TRANSFER == Action.TRANSFER:
rdp_options['drivestoredirect:s'] = '*' rdp_options['drivestoredirect:s'] = '*'
# 设置全屏 # 设置全屏
@ -181,22 +177,10 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
get_serializer: callable get_serializer: callable
perform_create: 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') @action(methods=['POST', 'GET'], detail=False, url_path='rdp/file')
def get_rdp_file(self, request, *args, **kwargs): def get_rdp_file(self, request, *args, **kwargs):
token = self.create_connection_token() token = self.create_connection_token()
self.check_token_permission(token) token.is_valid()
filename, content = self.get_rdp_file_info(token) filename, content = self.get_rdp_file_info(token)
filename = '{}.rdp'.format(filename) filename = '{}.rdp'.format(filename)
response = HttpResponse(content, content_type='application/octet-stream') 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') @action(methods=['POST', 'GET'], detail=False, url_path='client-url')
def get_client_protocol_url(self, request, *args, **kwargs): def get_client_protocol_url(self, request, *args, **kwargs):
token = self.create_connection_token() token = self.create_connection_token()
self.check_token_permission(token) token.is_valid()
try: try:
protocol_data = self.get_client_protocol_data(token) protocol_data = self.get_client_protocol_data(token)
except ValueError as e: except ValueError as e:
@ -224,12 +208,6 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
instance.expire() instance.expire()
return Response(status=status.HTTP_204_NO_CONTENT) 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): def create_connection_token(self):
data = self.request.query_params if self.request.method == 'GET' else self.request.data data = self.request.query_params if self.request.method == 'GET' else self.request.data
serializer = self.get_serializer(data=data) serializer = self.get_serializer(data=data)
@ -259,6 +237,18 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
'get_client_protocol_url': 'authentication.add_connectiontoken', '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): def dispatch(self, request, *args, **kwargs):
with tmp_to_root_org(): with tmp_to_root_org():
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
@ -296,9 +286,9 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
raise PermissionDenied('Expired') raise PermissionDenied('Expired')
if permed_account.has_secret: if permed_account.has_secret:
serializer.validated_data['secret'] = '' data['secret'] = ''
if permed_account.username != '@INPUT': if permed_account.username != '@INPUT':
serializer.validated_data['username'] = '' data['username'] = ''
return permed_account return permed_account

View File

@ -16,6 +16,11 @@ class Migration(migrations.Migration):
old_name='account_username', old_name='account_username',
new_name='login' new_name='login'
), ),
migrations.AlterField(
model_name='connectiontoken',
name='login',
field=models.CharField(max_length=128, verbose_name='Login account'),
),
migrations.AddField( migrations.AddField(
model_name='connectiontoken', model_name='connectiontoken',
name='username', name='username',

View File

@ -46,10 +46,6 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
('view_connectiontokensecret', _('Can view connection token secret')) ('view_connectiontokensecret', _('Can view connection token secret'))
] ]
@property
def is_valid(self):
return not self.is_expired
@property @property
def is_expired(self): def is_expired(self):
return self.date_expired < timezone.now() return self.date_expired < timezone.now()
@ -76,69 +72,71 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
self.date_expired = date_expired_default() self.date_expired = date_expired_default()
self.save() self.save()
# actions 和 expired_at 在 check_valid() 中赋值 @lazyproperty
actions = expire_at = None 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): @lazyproperty
from perms.utils.account import PermAccountUtil 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: if self.is_expired:
is_valid = False
error = _('Connection token expired at: {}').format(as_current_tz(self.date_expired)) 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: if not self.user or not self.user.is_valid:
is_valid = False
error = _('No user or invalid user') error = _('No user or invalid user')
return is_valid, error raise PermissionDenied(error)
if not self.asset or not self.asset.is_active: if not self.asset or not self.asset.is_active:
is_valid = False is_valid = False
error = _('No asset or inactive asset') error = _('No asset or inactive asset')
return is_valid, error return is_valid, error
if not self.login: if not self.login:
is_valid = False
error = _('No account') error = _('No account')
return is_valid, error raise PermissionDenied(error)
permed_account = PermAccountUtil().validate_permission( if not self.permed_account or not self.permed_account.actions:
self.user, self.asset, self.login
)
if not permed_account or not permed_account.actions:
msg = 'user `{}` not has asset `{}` permission for login `{}`'.format( msg = 'user `{}` not has asset `{}` permission for login `{}`'.format(
self.user, self.asset, self.login self.user, self.asset, self.login
) )
raise PermissionDenied(msg) raise PermissionDenied(msg)
if permed_account.date_expired < timezone.now(): if self.permed_account.date_expired < timezone.now():
raise PermissionDenied('Expired') raise PermissionDenied('Expired')
return True
is_valid, error = True, ''
return is_valid, error
@lazyproperty @lazyproperty
def platform(self): def platform(self):
return self.asset.platform return self.asset.platform
@lazyproperty @lazyproperty
def accounts(self): def account(self):
if not self.asset: if not self.asset:
return None return None
data = [] account = self.asset.accounts.filter(name=self.login).first()
if self.login == '@INPUT': if self.login == '@INPUT' or not account:
data.append({ return {
'name': self.login, 'name': self.login,
'username': self.username, 'username': self.username,
'secret_type': 'password', 'secret_type': 'password',
'secret': self.secret 'secret': self.secret
}) }
else: else:
accounts = self.asset.accounts.filter(username=self.login) return {
for account in accounts: 'name': account.name,
data.append({ 'username': account.username,
'username': account.uesrname, 'secret_type': account.secret_type,
'secret_type': account.secret_type, 'secret': account.secret_type or self.secret
'secret': account.secret if account.secret else self.secret }
})
return data
@lazyproperty @lazyproperty
def domain(self): def domain(self):

View File

@ -17,7 +17,6 @@ __all__ = [
class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
username = serializers.CharField(max_length=128, label=_("Input username"), username = serializers.CharField(max_length=128, label=_("Input username"),
allow_null=True, allow_blank=True) 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')) expire_time = serializers.IntegerField(read_only=True, label=_('Expired time'))
class Meta: class Meta:
@ -25,7 +24,7 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
fields_mini = ['id'] fields_mini = ['id']
fields_small = fields_mini + [ fields_small = fields_mini + [
'protocol', 'login', 'secret', 'username', 'protocol', 'login', 'secret', 'username',
'date_expired', 'date_created', 'actions', 'date_expired', 'date_created',
'date_updated', 'created_by', 'date_updated', 'created_by',
'updated_by', 'org_id', 'org_name', 'updated_by', 'org_id', 'org_name',
] ]
@ -34,7 +33,7 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
] ]
read_only_fields = [ read_only_fields = [
# 普通 Token 不支持指定 user # 普通 Token 不支持指定 user
'user', 'is_valid', 'expire_time', 'user', 'expire_time',
'user_display', 'asset_display', 'user_display', 'asset_display',
] ]
fields = fields_small + fields_fk + read_only_fields fields = fields_small + fields_fk + read_only_fields
@ -98,7 +97,7 @@ class ConnectionTokenAccountSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Account model = Account
fields = [ fields = [
'username', 'secret_type', 'secret', 'name', 'username', 'secret_type', 'secret',
] ]
@ -144,7 +143,7 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
user = ConnectionTokenUserSerializer(read_only=True) user = ConnectionTokenUserSerializer(read_only=True)
asset = ConnectionTokenAssetSerializer(read_only=True) asset = ConnectionTokenAssetSerializer(read_only=True)
platform = ConnectionTokenPlatform(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) gateway = ConnectionTokenGatewaySerializer(read_only=True)
# cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True) # cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True)
actions = ActionChoicesField() actions = ActionChoicesField()
@ -153,8 +152,7 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
class Meta: class Meta:
model = ConnectionToken model = ConnectionToken
fields = [ fields = [
'id', 'secret', 'user', 'asset', 'login', 'id', 'secret', 'user', 'asset', 'account',
'accounts', 'protocol', 'domain', 'gateway', 'protocol', 'domain', 'gateway',
'actions', 'expire_at', 'actions', 'expire_at', 'platform',
'platform',
] ]

View File

@ -1,4 +1,3 @@
from django.shortcuts import get_object_or_404
from rest_framework import viewsets from rest_framework import viewsets
from ops.models import Job, JobExecution from ops.models import Job, JobExecution
@ -7,14 +6,17 @@ from ops.serializers.job import JobSerializer, JobExecutionSerializer
__all__ = ['JobViewSet', 'JobExecutionViewSet'] __all__ = ['JobViewSet', 'JobExecutionViewSet']
from ops.tasks import run_ops_job, run_ops_job_executions 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 serializer_class = JobSerializer
queryset = Job.objects.all() model = Job
permission_classes = ()
def get_queryset(self): 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): def perform_create(self, serializer):
instance = serializer.save() instance = serializer.save()
@ -22,20 +24,20 @@ class JobViewSet(viewsets.ModelViewSet):
run_ops_job.delay(instance.id) run_ops_job.delay(instance.id)
class JobExecutionViewSet(viewsets.ModelViewSet): class JobExecutionViewSet(OrgBulkModelViewSet):
serializer_class = JobExecutionSerializer serializer_class = JobExecutionSerializer
queryset = JobExecution.objects.all()
http_method_names = ('get', 'post', 'head', 'options',) http_method_names = ('get', 'post', 'head', 'options',)
# filter_fields = ('type',)
permission_classes = ()
model = JobExecution
def perform_create(self, serializer): def perform_create(self, serializer):
instance = serializer.save() instance = serializer.save()
run_ops_job_executions.delay(instance.id) run_ops_job_executions.delay(instance.id)
def get_queryset(self): def get_queryset(self):
query_set = super().get_queryset()
job_id = self.request.query_params.get('job_id') job_id = self.request.query_params.get('job_id')
job_type = self.request.query_params.get('type')
if job_id: if job_id:
self.queryset = self.queryset.filter(job_id=job_id) self.queryset = query_set.filter(job_id=job_id)
if job_type: return query_set
self.queryset = self.queryset.filter(job__type=job_type)
return self.queryset

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -9,16 +9,14 @@ from django.utils.translation import gettext_lazy as _
from django.utils import timezone from django.utils import timezone
from celery import current_task from celery import current_task
from common.const.choices import Trigger
from common.db.models import BaseCreateUpdateModel
__all__ = ["Job", "JobExecution"] __all__ = ["Job", "JobExecution"]
from ops.ansible import JMSInventory, AdHocRunner, PlaybookRunner from ops.ansible import JMSInventory, AdHocRunner, PlaybookRunner
from ops.mixin import PeriodTaskModelMixin from ops.mixin import PeriodTaskModelMixin
from orgs.mixins.models import JMSOrgBaseModel
class Job(BaseCreateUpdateModel, PeriodTaskModelMixin): class Job(JMSOrgBaseModel, PeriodTaskModelMixin):
class Types(models.TextChoices): class Types(models.TextChoices):
adhoc = 'adhoc', _('Adhoc') adhoc = 'adhoc', _('Adhoc')
playbook = 'playbook', _('Playbook') playbook = 'playbook', _('Playbook')
@ -94,7 +92,7 @@ class Job(BaseCreateUpdateModel, PeriodTaskModelMixin):
return self.executions.create() return self.executions.create()
class JobExecution(BaseCreateUpdateModel): class JobExecution(JMSOrgBaseModel):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
task_id = models.UUIDField(null=True) task_id = models.UUIDField(null=True)
status = models.CharField(max_length=16, verbose_name=_('Status'), default='running') status = models.CharField(max_length=16, verbose_name=_('Status'), default='running')

View File

@ -1,14 +1,13 @@
from django.db import transaction
from rest_framework import serializers from rest_framework import serializers
from common.drf.fields import ReadableHiddenField from common.drf.fields import ReadableHiddenField
from ops.mixin import PeriodTaskSerializerMixin from ops.mixin import PeriodTaskSerializerMixin
from ops.models import Job, JobExecution from ops.models import Job, JobExecution
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
_all_ = [] _all_ = []
class JobSerializer(serializers.ModelSerializer, PeriodTaskSerializerMixin): class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin):
owner = ReadableHiddenField(default=serializers.CurrentUserDefault()) owner = ReadableHiddenField(default=serializers.CurrentUserDefault())
class Meta: class Meta:

View File

@ -10,6 +10,7 @@ from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.utils import get_logger, get_object_or_none, get_log_keep_day 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 ( from .celery.decorator import (
register_as_period_task, after_app_shutdown_clean_periodic, register_as_period_task, after_app_shutdown_clean_periodic,
after_app_ready_start 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")) @shared_task(soft_time_limit=60, queue="ansible", verbose_name=_("Run ansible task"))
def run_ops_job(job_id): def run_ops_job(job_id):
job = get_object_or_none(Job, id=job_id) job = get_object_or_none(Job, id=job_id)
execution = job.create_execution() with tmp_to_org(job.org):
try: execution = job.create_execution()
execution.start() try:
except SoftTimeLimitExceeded: execution.start()
execution.set_error('Run timeout') except SoftTimeLimitExceeded:
logger.error("Run adhoc timeout") execution.set_error('Run timeout')
except Exception as e: logger.error("Run adhoc timeout")
execution.set_error(e) except Exception as e:
logger.error("Start adhoc execution error: {}".format(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")) @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_executions(execution_id, **kwargs):
execution = get_object_or_none(JobExecution, id=execution_id) execution = get_object_or_none(JobExecution, id=execution_id)
try: with tmp_to_org(execution.org):
execution.start() try:
except SoftTimeLimitExceeded: execution.start()
execution.set_error('Run timeout') except SoftTimeLimitExceeded:
logger.error("Run adhoc timeout") execution.set_error('Run timeout')
except Exception as e: logger.error("Run adhoc timeout")
execution.set_error(e) except Exception as e:
logger.error("Start adhoc execution error: {}".format(e)) execution.set_error(e)
logger.error("Start adhoc execution error: {}".format(e))
@shared_task(verbose_name=_('Periodic clear celery tasks')) @shared_task(verbose_name=_('Periodic clear celery tasks'))

View File

@ -28,8 +28,16 @@ class ActionChoices(BitChoices):
) )
@classmethod @classmethod
def has_perm(cls, action_name, total): def transfer(cls):
action_value = getattr(cls, action_name) 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 return action_value & total == action_value
@classmethod @classmethod