Merge branch 'v3' of https://github.com/jumpserver/jumpserver into pr@v3@feat_db_automations

pull/9035/head
jiangweidong 2022-11-09 18:23:10 +08:00
commit be875638ed
65 changed files with 877 additions and 310 deletions

View File

@ -6,5 +6,6 @@ from .label import *
from .account import * from .account import *
from .node import * from .node import *
from .domain import * from .domain import *
from .automations import *
from .gathered_user import * from .gathered_user import *
from .favorite_asset import * from .favorite_asset import *

View File

@ -82,10 +82,11 @@ class AssetsTaskMixin:
def perform_assets_task(self, serializer): def perform_assets_task(self, serializer):
data = serializer.validated_data data = serializer.validated_data
assets = data.get('assets', []) assets = data.get('assets', [])
asset_ids = [asset.id for asset in assets]
if data['action'] == "refresh": if data['action'] == "refresh":
task = update_assets_hardware_info_manual.delay(assets) task = update_assets_hardware_info_manual.delay(asset_ids)
else: else:
task = test_assets_connectivity_manual.delay(assets) task = test_assets_connectivity_manual.delay(asset_ids)
return task return task
def perform_create(self, serializer): def perform_create(self, serializer):

View File

@ -0,0 +1,3 @@
from .base import *
from .change_secret import *
from .gather_accounts import *

View File

@ -0,0 +1,118 @@
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _
from rest_framework.response import Response
from rest_framework import status, mixins, viewsets
from orgs.mixins import generics
from assets import serializers
from assets.const import AutomationTypes
from assets.tasks import execute_automation
from assets.models import BaseAutomation, AutomationExecution
from common.const.choices import Trigger
__all__ = [
'AutomationAssetsListApi', 'AutomationRemoveAssetApi',
'AutomationAddAssetApi', 'AutomationNodeAddRemoveApi', 'AutomationExecutionViewSet'
]
class AutomationAssetsListApi(generics.ListAPIView):
serializer_class = serializers.AutomationAssetsSerializer
filter_fields = ("name", "address")
search_fields = filter_fields
def get_object(self):
pk = self.kwargs.get('pk')
return get_object_or_404(BaseAutomation, pk=pk)
def get_queryset(self):
instance = self.get_object()
assets = instance.get_all_assets().only(
*self.serializer_class.Meta.only_fields
)
return assets
class AutomationRemoveAssetApi(generics.RetrieveUpdateAPIView):
model = BaseAutomation
serializer_class = serializers.UpdateAssetSerializer
def update(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.serializer_class(data=request.data)
if not serializer.is_valid():
return Response({'error': serializer.errors})
assets = serializer.validated_data.get('assets')
if assets:
instance.assets.remove(*tuple(assets))
return Response({'msg': 'ok'})
class AutomationAddAssetApi(generics.RetrieveUpdateAPIView):
model = BaseAutomation
serializer_class = serializers.UpdateAssetSerializer
def update(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
assets = serializer.validated_data.get('assets')
if assets:
instance.assets.add(*tuple(assets))
return Response({"msg": "ok"})
else:
return Response({"error": serializer.errors})
class AutomationNodeAddRemoveApi(generics.RetrieveUpdateAPIView):
model = BaseAutomation
serializer_class = serializers.UpdateAssetSerializer
def update(self, request, *args, **kwargs):
action_params = ['add', 'remove']
action = request.query_params.get('action')
if action not in action_params:
err_info = _("The parameter 'action' must be [{}]".format(','.join(action_params)))
return Response({"error": err_info})
instance = self.get_object()
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
nodes = serializer.validated_data.get('nodes')
if nodes:
# eg: plan.nodes.add(*tuple(assets))
getattr(instance.nodes, action)(*tuple(nodes))
return Response({"msg": "ok"})
else:
return Response({"error": serializer.errors})
class AutomationExecutionViewSet(
mixins.CreateModelMixin, mixins.ListModelMixin,
mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
search_fields = ('trigger',)
filterset_fields = ('trigger', 'automation_id')
serializer_class = serializers.AutomationExecutionSerializer
def get_queryset(self):
queryset = AutomationExecution.objects.all()
return queryset
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
queryset = queryset.order_by('-date_start')
return queryset
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
automation = serializer.validated_data.get('automation')
tp = serializer.validated_data.get('type')
model = AutomationTypes.get_type_model(tp)
task = execute_automation.delay(
pid=automation.pk, trigger=Trigger.manual, model=model
)
return Response({'task': task.id}, status=status.HTTP_201_CREATED)

View File

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
#
from rest_framework import mixins
from common.utils import get_object_or_none
from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet
from assets.models import ChangeSecretAutomation, ChangeSecretRecord, AutomationExecution
from assets import serializers
__all__ = [
'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet'
]
class ChangeSecretAutomationViewSet(OrgBulkModelViewSet):
model = ChangeSecretAutomation
filter_fields = ('name', 'secret_type', 'secret_strategy')
search_fields = filter_fields
ordering_fields = ('name',)
serializer_class = serializers.ChangeSecretAutomationSerializer
class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
serializer_class = serializers.ChangeSecretRecordSerializer
filter_fields = ['asset', 'execution_id']
search_fields = ['asset__hostname']
def get_queryset(self):
return ChangeSecretRecord.objects.all()
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
eid = self.request.GET.get('execution_id')
execution = get_object_or_none(AutomationExecution, pk=eid)
if execution:
queryset = queryset.filter(execution=execution)
queryset = queryset.order_by('is_success', '-date_start')
return queryset

View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
#
from orgs.mixins.api import OrgBulkModelViewSet
from assets.models import GatherAccountsAutomation
from assets import serializers
__all__ = [
'GatherAccountsAutomationViewSet',
]
class GatherAccountsAutomationViewSet(OrgBulkModelViewSet):
model = GatherAccountsAutomation
filter_fields = ('name',)
search_fields = filter_fields
ordering_fields = ('name',)
serializer_class = serializers.GatherAccountAutomationSerializer

View File

@ -47,7 +47,7 @@ class PushOrVerifyHostCallbackMixin:
secret = account.secret secret = account.secret
private_key_path = None private_key_path = None
if account.secret_type == SecretType.ssh_key: if account.secret_type == SecretType.SSH_KEY:
private_key_path = self.generate_private_key_path(secret, path_dir) private_key_path = self.generate_private_key_path(secret, path_dir)
secret = self.generate_public_key(secret) secret = self.generate_public_key(secret)
@ -221,6 +221,7 @@ class BasePlaybookManager:
else: else:
print(">>> 开始执行任务\n") print(">>> 开始执行任务\n")
self.execution.date_start = timezone.now()
for i, runner in enumerate(runners, start=1): for i, runner in enumerate(runners, start=1):
if len(runners) > 1: if len(runners) > 1:
print(">>> 开始执行第 {} 批任务".format(i)) print(">>> 开始执行第 {} 批任务".format(i))
@ -231,3 +232,6 @@ class BasePlaybookManager:
except Exception as e: except Exception as e:
self.on_runner_failed(runner, e) self.on_runner_failed(runner, e)
print('\n') print('\n')
self.execution.status = 'success'
self.execution.date_finished = timezone.now()
self.execution.save()

View File

@ -89,9 +89,9 @@ class ChangeSecretManager(BasePlaybookManager):
return self.generate_password() return self.generate_password()
def get_secret(self): def get_secret(self):
if self.secret_type == SecretType.ssh_key: if self.secret_type == SecretType.SSH_KEY:
secret = self.get_ssh_key() secret = self.get_ssh_key()
elif self.secret_type == SecretType.password: elif self.secret_type == SecretType.PASSWORD:
secret = self.get_password() secret = self.get_password()
else: else:
raise ValueError("Secret must be set") raise ValueError("Secret must be set")
@ -99,7 +99,7 @@ class ChangeSecretManager(BasePlaybookManager):
def get_kwargs(self, account, secret): def get_kwargs(self, account, secret):
kwargs = {} kwargs = {}
if self.secret_type != SecretType.ssh_key: if self.secret_type != SecretType.SSH_KEY:
return kwargs return kwargs
kwargs['strategy'] = self.execution.snapshot['ssh_key_change_strategy'] kwargs['strategy'] = self.execution.snapshot['ssh_key_change_strategy']
kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no' kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no'
@ -143,7 +143,7 @@ class ChangeSecretManager(BasePlaybookManager):
self.name_recorder_mapper[h['name']] = recorder self.name_recorder_mapper[h['name']] = recorder
private_key_path = None private_key_path = None
if self.secret_type == SecretType.ssh_key: if self.secret_type == SecretType.SSH_KEY:
private_key_path = self.generate_private_key_path(new_secret, path_dir) private_key_path = self.generate_private_key_path(new_secret, path_dir)
new_secret = self.generate_public_key(new_secret) new_secret = self.generate_public_key(new_secret)

View File

@ -4,16 +4,18 @@ from .gather_accounts.manager import GatherAccountsManager
from .verify_account.manager import VerifyAccountManager from .verify_account.manager import VerifyAccountManager
from .push_account.manager import PushAccountManager from .push_account.manager import PushAccountManager
from .backup_account.manager import AccountBackupManager from .backup_account.manager import AccountBackupManager
from .ping.manager import PingManager
from ..const import AutomationTypes from ..const import AutomationTypes
class ExecutionManager: class ExecutionManager:
manager_type_mapper = { manager_type_mapper = {
AutomationTypes.change_secret: ChangeSecretManager, AutomationTypes.ping: PingManager,
AutomationTypes.gather_facts: GatherFactsManager,
AutomationTypes.gather_accounts: GatherAccountsManager,
AutomationTypes.verify_account: VerifyAccountManager,
AutomationTypes.push_account: PushAccountManager, AutomationTypes.push_account: PushAccountManager,
AutomationTypes.gather_facts: GatherFactsManager,
AutomationTypes.change_secret: ChangeSecretManager,
AutomationTypes.verify_account: VerifyAccountManager,
AutomationTypes.gather_accounts: GatherAccountsManager,
# TODO 后期迁移到自动化策略中 # TODO 后期迁移到自动化策略中
'backup_account': AccountBackupManager, 'backup_account': AccountBackupManager,
} }

View File

@ -21,14 +21,14 @@ class PingManager(BasePlaybookManager):
def on_host_success(self, host, result): def on_host_success(self, host, result):
asset, account = self.host_asset_and_account_mapper.get(host) asset, account = self.host_asset_and_account_mapper.get(host)
asset.set_connectivity(Connectivity.ok) asset.set_connectivity(Connectivity.OK)
if not account: if not account:
return return
account.set_connectivity(Connectivity.ok) account.set_connectivity(Connectivity.OK)
def on_host_error(self, host, error, result): def on_host_error(self, host, error, result):
asset, account = self.host_asset_and_account_mapper.get(host) asset, account = self.host_asset_and_account_mapper.get(host)
asset.set_connectivity(Connectivity.failed) asset.set_connectivity(Connectivity.FAILED)
if not account: if not account:
return return
account.set_connectivity(Connectivity.failed) account.set_connectivity(Connectivity.FAILED)

View File

@ -18,8 +18,8 @@ class VerifyAccountManager(PushOrVerifyHostCallbackMixin, BasePlaybookManager):
def on_host_success(self, host, result): def on_host_success(self, host, result):
account = self.host_account_mapper.get(host) account = self.host_account_mapper.get(host)
account.set_connectivity(Connectivity.ok) account.set_connectivity(Connectivity.OK)
def on_host_error(self, host, error, result): def on_host_error(self, host, error, result):
account = self.host_account_mapper.get(host) account = self.host_account_mapper.get(host)
account.set_connectivity(Connectivity.failed) account.set_connectivity(Connectivity.FAILED)

View File

@ -3,13 +3,13 @@ from django.utils.translation import ugettext_lazy as _
class Connectivity(TextChoices): class Connectivity(TextChoices):
unknown = 'unknown', _('Unknown') UNKNOWN = 'unknown', _('Unknown')
ok = 'ok', _('Ok') OK = 'ok', _('Ok')
failed = 'failed', _('Failed') FAILED = 'failed', _('Failed')
class SecretType(TextChoices): class SecretType(TextChoices):
password = 'password', _('Password') PASSWORD = 'password', _('Password')
ssh_key = 'ssh_key', _('SSH key') SSH_KEY = 'ssh_key', _('SSH key')
access_key = 'access_key', _('Access key') ACCESS_KEY = 'access_key', _('Access key')
token = 'token', _('Token') TOKEN = 'token', _('Token')

View File

@ -17,6 +17,22 @@ class AutomationTypes(TextChoices):
verify_account = 'verify_account', _('Verify account') verify_account = 'verify_account', _('Verify account')
gather_accounts = 'gather_accounts', _('Gather accounts') gather_accounts = 'gather_accounts', _('Gather accounts')
@classmethod
def get_type_model(cls, tp):
from assets.models import (
PingAutomation, GatherFactsAutomation, PushAccountAutomation,
ChangeSecretAutomation, VerifyAccountAutomation, GatherAccountsAutomation,
)
type_model_dict = {
cls.ping: PingAutomation,
cls.gather_facts: GatherFactsAutomation,
cls.push_account: PushAccountAutomation,
cls.change_secret: ChangeSecretAutomation,
cls.verify_account: VerifyAccountAutomation,
cls.gather_accounts: GatherAccountsAutomation,
}
return type_model_dict.get(tp)
class SecretStrategy(TextChoices): class SecretStrategy(TextChoices):
custom = 'specific', _('Specific') custom = 'specific', _('Specific')

View File

@ -1,7 +1,8 @@
from .change_secret import * from .ping import *
from .discovery_account import * from .base import *
from .push_account import * from .push_account import *
from .gather_facts import * from .gather_facts import *
from .gather_accounts import * from .change_secret import *
from .verify_account import * from .verify_account import *
from .ping import * from .gather_accounts import *
from .discovery_account import *

View File

@ -3,7 +3,7 @@ from celery import current_task
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.const.choices import Trigger, Status from common.const.choices import Trigger
from common.mixins.models import CommonModelMixin from common.mixins.models import CommonModelMixin
from common.db.fields import EncryptJsonDictTextField from common.db.fields import EncryptJsonDictTextField
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
@ -15,12 +15,8 @@ from assets.const import AutomationTypes
class BaseAutomation(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin): class BaseAutomation(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin):
accounts = models.JSONField(default=list, verbose_name=_("Accounts")) accounts = models.JSONField(default=list, verbose_name=_("Accounts"))
nodes = models.ManyToManyField( nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
'assets.Node', blank=True, verbose_name=_("Nodes") assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
)
assets = models.ManyToManyField(
'assets.Asset', blank=True, verbose_name=_("Assets")
)
type = models.CharField(max_length=16, choices=AutomationTypes.choices, verbose_name=_('Type')) type = models.CharField(max_length=16, choices=AutomationTypes.choices, verbose_name=_('Type'))
is_active = models.BooleanField(default=True, verbose_name=_("Is active")) is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
comment = models.TextField(blank=True, verbose_name=_('Comment')) comment = models.TextField(blank=True, verbose_name=_('Comment'))
@ -92,7 +88,7 @@ class AutomationExecution(OrgModelMixin):
'BaseAutomation', related_name='executions', on_delete=models.CASCADE, 'BaseAutomation', related_name='executions', on_delete=models.CASCADE,
verbose_name=_('Automation task') verbose_name=_('Automation task')
) )
status = models.CharField(max_length=16, default='pending') status = models.CharField(max_length=16, default='pending', verbose_name=_('Status'))
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created')) date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created'))
date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True) date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True)
date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished")) date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished"))

View File

@ -12,7 +12,7 @@ __all__ = ['ChangeSecretAutomation', 'ChangeSecretRecord']
class ChangeSecretAutomation(BaseAutomation): class ChangeSecretAutomation(BaseAutomation):
secret_type = models.CharField( secret_type = models.CharField(
choices=SecretType.choices, max_length=16, choices=SecretType.choices, max_length=16,
default=SecretType.password, verbose_name=_('Secret type') default=SecretType.PASSWORD, verbose_name=_('Secret type')
) )
secret_strategy = models.CharField( secret_strategy = models.CharField(
choices=SecretStrategy.choices, max_length=16, choices=SecretStrategy.choices, max_length=16,
@ -24,7 +24,7 @@ class ChangeSecretAutomation(BaseAutomation):
choices=SSHKeyStrategy.choices, max_length=16, choices=SSHKeyStrategy.choices, max_length=16,
default=SSHKeyStrategy.add, verbose_name=_('SSH key change strategy') default=SSHKeyStrategy.add, verbose_name=_('SSH key change strategy')
) )
recipients = models.ManyToManyField('users.User', blank=True, verbose_name=_("Recipient")) recipients = models.ManyToManyField('users.User', verbose_name=_("Recipient"), blank=True)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.type = AutomationTypes.change_secret self.type = AutomationTypes.change_secret

View File

@ -13,3 +13,7 @@ class GatherAccountsAutomation(BaseAutomation):
class Meta: class Meta:
verbose_name = _("Gather asset accounts") verbose_name = _("Gather asset accounts")
@property
def executed_amount(self):
return self.executions.count()

View File

@ -24,7 +24,7 @@ logger = get_logger(__file__)
class AbsConnectivity(models.Model): class AbsConnectivity(models.Model):
connectivity = models.CharField( connectivity = models.CharField(
choices=Connectivity.choices, default=Connectivity.unknown, choices=Connectivity.choices, default=Connectivity.UNKNOWN,
max_length=16, verbose_name=_('Connectivity') max_length=16, verbose_name=_('Connectivity')
) )
date_verified = models.DateTimeField(null=True, verbose_name=_("Date verified")) date_verified = models.DateTimeField(null=True, verbose_name=_("Date verified"))
@ -50,7 +50,7 @@ class BaseAccount(JMSOrgBaseModel):
name = models.CharField(max_length=128, verbose_name=_("Name")) name = models.CharField(max_length=128, verbose_name=_("Name"))
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True) username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True)
secret_type = models.CharField( secret_type = models.CharField(
max_length=16, choices=SecretType.choices, default=SecretType.password, verbose_name=_('Secret type') max_length=16, choices=SecretType.choices, default=SecretType.PASSWORD, verbose_name=_('Secret type')
) )
secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret')) secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
privileged = models.BooleanField(verbose_name=_("Privileged"), default=False) privileged = models.BooleanField(verbose_name=_("Privileged"), default=False)
@ -65,25 +65,25 @@ class BaseAccount(JMSOrgBaseModel):
@property @property
def specific(self): def specific(self):
data = {} data = {}
if self.secret_type != SecretType.ssh_key: if self.secret_type != SecretType.SSH_KEY:
return data return data
data['ssh_key_fingerprint'] = self.ssh_key_fingerprint data['ssh_key_fingerprint'] = self.ssh_key_fingerprint
return data return data
@property @property
def private_key(self): def private_key(self):
if self.secret_type == SecretType.ssh_key: if self.secret_type == SecretType.SSH_KEY:
return self.secret return self.secret
return None return None
@private_key.setter @private_key.setter
def private_key(self, value): def private_key(self, value):
self.secret = value self.secret = value
self.secret_type = SecretType.ssh_key self.secret_type = SecretType.SSH_KEY
@lazyproperty @lazyproperty
def public_key(self): def public_key(self):
if self.secret_type == SecretType.ssh_key: if self.secret_type == SecretType.SSH_KEY:
return ssh_pubkey_gen(private_key=self.private_key) return ssh_pubkey_gen(private_key=self.private_key)
return None return None
@ -113,7 +113,7 @@ class BaseAccount(JMSOrgBaseModel):
@property @property
def private_key_path(self): def private_key_path(self):
if not self.secret_type != SecretType.ssh_key or not self.secret: if not self.secret_type != SecretType.SSH_KEY or not self.secret:
return None return None
project_dir = settings.PROJECT_DIR project_dir = settings.PROJECT_DIR
tmp_dir = os.path.join(project_dir, 'tmp') tmp_dir = os.path.join(project_dir, 'tmp')

View File

@ -201,7 +201,7 @@ class CommandFilterRule(OrgModelMixin):
q |= Q(user_groups__in=set(user_groups)) q |= Q(user_groups__in=set(user_groups))
if account: if account:
org_id = account.org_id org_id = account.org_id
q |= Q(accounts__contains=list(account)) |\ q |= Q(accounts__contains=account.username) | \
Q(accounts__contains=SpecialAccount.ALL.value) Q(accounts__contains=SpecialAccount.ALL.value)
if asset: if asset:
org_id = asset.org_id org_id = asset.org_id

View File

@ -6,7 +6,6 @@ from common.db.fields import JsonDictTextField
from assets.const import Protocol from assets.const import Protocol
__all__ = ['Platform', 'PlatformProtocol', 'PlatformAutomation'] __all__ = ['Platform', 'PlatformProtocol', 'PlatformAutomation']
@ -49,11 +48,15 @@ class PlatformAutomation(models.Model):
push_account_enabled = models.BooleanField(default=False, verbose_name=_("Push account enabled")) push_account_enabled = models.BooleanField(default=False, verbose_name=_("Push account enabled"))
push_account_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Push account method")) push_account_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Push account method"))
change_secret_enabled = models.BooleanField(default=False, verbose_name=_("Change password enabled")) change_secret_enabled = models.BooleanField(default=False, verbose_name=_("Change password enabled"))
change_secret_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Change password method")) change_secret_method = models.TextField(
max_length=32, blank=True, null=True, verbose_name=_("Change password method"))
verify_account_enabled = models.BooleanField(default=False, verbose_name=_("Verify account enabled")) verify_account_enabled = models.BooleanField(default=False, verbose_name=_("Verify account enabled"))
verify_account_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Verify account method")) verify_account_method = models.TextField(
max_length=32, blank=True, null=True, verbose_name=_("Verify account method"))
gather_accounts_enabled = models.BooleanField(default=False, verbose_name=_("Gather facts enabled")) gather_accounts_enabled = models.BooleanField(default=False, verbose_name=_("Gather facts enabled"))
gather_accounts_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Gather facts method")) gather_accounts_method = models.TextField(
max_length=32, blank=True, null=True, verbose_name=_("Gather facts method")
)
class Platform(models.Model): class Platform(models.Model):
@ -61,10 +64,11 @@ class Platform(models.Model):
对资产提供 约束和默认值 对资产提供 约束和默认值
对资产进行抽象 对资产进行抽象
""" """
CHARSET_CHOICES = (
('utf8', 'UTF-8'), class CharsetChoices(models.TextChoices):
('gbk', 'GBK'), utf8 = 'utf8', 'UTF-8'
) gbk = 'gbk', 'GBK'
name = models.SlugField(verbose_name=_("Name"), unique=True, allow_unicode=True) name = models.SlugField(verbose_name=_("Name"), unique=True, allow_unicode=True)
category = models.CharField(default='host', max_length=32, verbose_name=_("Category")) category = models.CharField(default='host', max_length=32, verbose_name=_("Category"))
type = models.CharField(max_length=32, default='linux', verbose_name=_("Type")) type = models.CharField(max_length=32, default='linux', verbose_name=_("Type"))
@ -72,7 +76,9 @@ class Platform(models.Model):
internal = models.BooleanField(default=False, verbose_name=_("Internal")) internal = models.BooleanField(default=False, verbose_name=_("Internal"))
comment = models.TextField(blank=True, null=True, verbose_name=_("Comment")) comment = models.TextField(blank=True, null=True, verbose_name=_("Comment"))
# 资产有关的 # 资产有关的
charset = models.CharField(default='utf8', choices=CHARSET_CHOICES, max_length=8, verbose_name=_("Charset")) charset = models.CharField(
default=CharsetChoices.utf8, choices=CharsetChoices.choices, max_length=8, verbose_name=_("Charset")
)
domain_enabled = models.BooleanField(default=True, verbose_name=_("Domain enabled")) domain_enabled = models.BooleanField(default=True, verbose_name=_("Domain enabled"))
protocols_enabled = models.BooleanField(default=True, verbose_name=_("Protocols enabled")) protocols_enabled = models.BooleanField(default=True, verbose_name=_("Protocols enabled"))
# 账号有关的 # 账号有关的
@ -103,4 +109,3 @@ class Platform(models.Model):
class Meta: class Meta:
verbose_name = _("Platform") verbose_name = _("Platform")
# ordering = ('name',) # ordering = ('name',)

View File

@ -11,4 +11,4 @@ from .account import *
from assets.serializers.account.backup import * from assets.serializers.account.backup import *
from .platform import * from .platform import *
from .cagegory import * from .cagegory import *
from .automation import * from .automations import *

View File

@ -2,10 +2,11 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from common.drf.serializers import SecretReadableMixin from common.drf.serializers import SecretReadableMixin
from common.drf.fields import ObjectRelatedField from common.drf.fields import ObjectRelatedField, LabeledChoiceField
from assets.tasks import push_accounts_to_assets from assets.tasks import push_accounts_to_assets
from assets.models import Account, AccountTemplate, Asset from assets.models import Account, AccountTemplate, Asset
from .base import BaseAccountSerializer from .base import BaseAccountSerializer
from assets.const import SecretType
class AccountSerializerCreateMixin(serializers.ModelSerializer): class AccountSerializerCreateMixin(serializers.ModelSerializer):
@ -91,6 +92,8 @@ class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
class AccountHistorySerializer(serializers.ModelSerializer): class AccountHistorySerializer(serializers.ModelSerializer):
secret_type = LabeledChoiceField(choices=SecretType.choices, label=_('Secret type'))
class Meta: class Meta:
model = Account.history.model model = Account.history.model
fields = ['id', 'secret', 'secret_type', 'version', 'history_date', 'history_user'] fields = ['id', 'secret', 'secret_type', 'version', 'history_date', 'history_user']

View File

@ -6,6 +6,8 @@ from rest_framework import serializers
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ops.mixin import PeriodTaskSerializerMixin from ops.mixin import PeriodTaskSerializerMixin
from common.utils import get_logger from common.utils import get_logger
from common.const.choices import Trigger
from common.drf.fields import LabeledChoiceField
from assets.models import AccountBackupPlan, AccountBackupPlanExecution from assets.models import AccountBackupPlan, AccountBackupPlanExecution
@ -20,7 +22,7 @@ class AccountBackupPlanSerializer(PeriodTaskSerializerMixin, BulkOrgResourceMode
fields = [ fields = [
'id', 'name', 'is_periodic', 'interval', 'crontab', 'date_created', 'id', 'name', 'is_periodic', 'interval', 'crontab', 'date_created',
'date_updated', 'created_by', 'periodic_display', 'comment', 'date_updated', 'created_by', 'periodic_display', 'comment',
'recipients', 'categories' 'recipients', 'types'
] ]
extra_kwargs = { extra_kwargs = {
'name': {'required': True}, 'name': {'required': True},
@ -32,17 +34,12 @@ class AccountBackupPlanSerializer(PeriodTaskSerializerMixin, BulkOrgResourceMode
class AccountBackupPlanExecutionSerializer(serializers.ModelSerializer): class AccountBackupPlanExecutionSerializer(serializers.ModelSerializer):
trigger_display = serializers.ReadOnlyField( trigger = LabeledChoiceField(choices=Trigger.choices, label=_('Trigger mode'))
source='get_trigger_display', label=_('Trigger mode')
)
class Meta: class Meta:
model = AccountBackupPlanExecution model = AccountBackupPlanExecution
fields = [ read_only_fields = [
'id', 'date_start', 'timedelta', 'plan_snapshot', 'trigger', 'reason',
'is_success', 'plan', 'org_id', 'recipients', 'trigger_display'
]
read_only_fields = (
'id', 'date_start', 'timedelta', 'plan_snapshot', 'trigger', 'reason', 'id', 'date_start', 'timedelta', 'plan_snapshot', 'trigger', 'reason',
'is_success', 'org_id', 'recipients' 'is_success', 'org_id', 'recipients'
) ]
fields = read_only_fields + ['plan']

View File

@ -1,27 +1,21 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from io import StringIO
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from common.utils import validate_ssh_private_key, ssh_private_key_gen
from common.drf.fields import EncryptedField
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from assets.models import BaseAccount from assets.models import BaseAccount
from assets.serializers.base import AuthValidateMixin
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
__all__ = ['BaseAccountSerializer'] __all__ = ['BaseAccountSerializer']
class BaseAccountSerializer(BulkOrgResourceModelSerializer): class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
secret = EncryptedField(
label=_('Secret'), required=False, allow_blank=True,
allow_null=True, max_length=40960
)
class Meta: class Meta:
model = BaseAccount model = BaseAccount
fields_mini = ['id', 'name', 'username'] fields_mini = ['id', 'name', 'username']
fields_small = fields_mini + ['privileged', 'secret_type', 'secret', 'has_secret', 'specific'] fields_small = fields_mini + [
'secret_type', 'secret', 'has_secret', 'passphrase',
'privileged', 'is_active', 'specific',
]
fields_other = ['created_by', 'date_created', 'date_updated', 'comment'] fields_other = ['created_by', 'date_created', 'date_updated', 'comment']
fields = fields_small + fields_other fields = fields_small + fields_other
read_only_fields = [ read_only_fields = [
@ -29,28 +23,5 @@ class BaseAccountSerializer(BulkOrgResourceModelSerializer):
'date_verified', 'created_by', 'date_created', 'date_verified', 'created_by', 'date_created',
] ]
extra_kwargs = { extra_kwargs = {
'secret': {'write_only': True}, 'specific': {'label': _('Specific')},
'passphrase': {'write_only': True},
} }
def validate_private_key(self, private_key):
if not private_key:
return ''
passphrase = self.initial_data.get('passphrase')
passphrase = passphrase if passphrase else None
valid = validate_ssh_private_key(private_key, password=passphrase)
if not valid:
raise serializers.ValidationError(_("private key invalid or passphrase error"))
private_key = ssh_private_key_gen(private_key, password=passphrase)
string_io = StringIO()
private_key.write_private_key(string_io)
private_key = string_io.getvalue()
return private_key
def validate_secret(self, value):
secret_type = self.initial_data.get('secret_type')
if secret_type == 'ssh_key':
value = self.validate_private_key(value)
return value

View File

@ -77,7 +77,7 @@ class AssetSerializer(OrgResourceSerializerMixin, WritableNestedModelSerializer)
'nodes', 'labels', 'protocols', 'accounts', 'nodes_display', 'nodes', 'labels', 'protocols', 'accounts', 'nodes_display',
] ]
read_only_fields = [ read_only_fields = [
'category', 'type', 'specific', 'category', 'type', 'specific', 'info',
'connectivity', 'date_verified', 'connectivity', 'date_verified',
'created_by', 'date_created', 'created_by', 'date_created',
] ]

View File

@ -1,35 +0,0 @@
from django.utils.translation import ugettext as _
from rest_framework import serializers
from common.utils import get_logger
from assets.models import ChangeSecretRecord
logger = get_logger(__file__)
class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer):
asset = serializers.SerializerMethodField(label=_('Asset'))
account = serializers.SerializerMethodField(label=_('Account'))
is_success = serializers.SerializerMethodField(label=_('Is success'))
class Meta:
model = ChangeSecretRecord
fields = [
'id', 'asset', 'account', 'old_secret', 'new_secret',
'status', 'error', 'is_success'
]
@staticmethod
def get_asset(instance):
return str(instance.asset)
@staticmethod
def get_account(instance):
return str(instance.account)
@staticmethod
def get_is_success(obj):
if obj.status == 'success':
return _("Success")
return _("Failed")

View File

@ -0,0 +1,3 @@
from .base import *
from .change_secret import *
from .gather_accounts import *

View File

@ -0,0 +1,81 @@
from django.utils.translation import ugettext as _
from rest_framework import serializers
from ops.mixin import PeriodTaskSerializerMixin
from assets.const import AutomationTypes
from assets.models import Asset, Node, BaseAutomation, AutomationExecution
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.utils import get_logger
from common.drf.fields import ObjectRelatedField
logger = get_logger(__file__)
__all__ = [
'BaseAutomationSerializer', 'AutomationExecutionSerializer',
'UpdateAssetSerializer', 'UpdateNodeSerializer', 'AutomationAssetsSerializer',
]
class BaseAutomationSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer):
assets = ObjectRelatedField(many=True, required=False, queryset=Asset.objects, label=_('Assets'))
nodes = ObjectRelatedField(many=True, required=False, queryset=Node.objects, label=_('Nodes'))
class Meta:
read_only_fields = [
'date_created', 'date_updated', 'created_by', 'periodic_display'
]
fields = read_only_fields + [
'id', 'name', 'is_periodic', 'interval', 'crontab', 'comment',
'type', 'accounts', 'nodes', 'assets', 'is_active'
]
extra_kwargs = {
'name': {'required': True},
'type': {'read_only': True},
'periodic_display': {'label': _('Periodic perform')},
}
class AutomationExecutionSerializer(serializers.ModelSerializer):
snapshot = serializers.SerializerMethodField(label=_('Automation snapshot'))
type = serializers.ChoiceField(choices=AutomationTypes.choices, write_only=True, label=_('Type'))
trigger_display = serializers.ReadOnlyField(source='get_trigger_display', label=_('Trigger mode'))
class Meta:
model = AutomationExecution
read_only_fields = [
'trigger_display', 'date_start', 'date_finished', 'snapshot', 'status'
]
fields = ['id', 'automation', 'trigger', 'type'] + read_only_fields
@staticmethod
def get_snapshot(obj):
tp = obj.snapshot['type']
snapshot = {
'type': tp,
'name': obj.snapshot['name'],
'comment': obj.snapshot['comment'],
'accounts': obj.snapshot['accounts'],
'node_amount': len(obj.snapshot['nodes']),
'asset_amount': len(obj.snapshot['assets']),
'type_display': getattr(AutomationTypes, tp).label,
}
return snapshot
class UpdateAssetSerializer(serializers.ModelSerializer):
class Meta:
model = BaseAutomation
fields = ['id', 'assets']
class UpdateNodeSerializer(serializers.ModelSerializer):
class Meta:
model = BaseAutomation
fields = ['id', 'nodes']
class AutomationAssetsSerializer(serializers.ModelSerializer):
class Meta:
model = Asset
only_fields = ['id', 'name', 'address']
fields = tuple(only_fields)

View File

@ -0,0 +1,133 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext as _
from rest_framework import serializers
from common.utils import get_logger
from common.drf.fields import LabeledChoiceField, ObjectRelatedField
from assets.serializers.base import AuthValidateMixin
from assets.const import DEFAULT_PASSWORD_RULES, SecretType, SecretStrategy, SSHKeyStrategy
from assets.models import Asset, Account, ChangeSecretAutomation, ChangeSecretRecord, AutomationExecution
from .base import BaseAutomationSerializer
logger = get_logger(__file__)
__all__ = [
'ChangeSecretAutomationSerializer',
'ChangeSecretRecordSerializer',
'ChangeSecretRecordBackUpSerializer'
]
class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializer):
secret_strategy = LabeledChoiceField(
choices=SecretStrategy.choices, required=True, label=_('Secret strategy')
)
ssh_key_change_strategy = LabeledChoiceField(
choices=SSHKeyStrategy.choices, required=False, label=_('SSH Key strategy')
)
password_rules = serializers.DictField(default=DEFAULT_PASSWORD_RULES)
class Meta:
model = ChangeSecretAutomation
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields
fields = BaseAutomationSerializer.Meta.fields + read_only_fields + [
'secret_type', 'secret_strategy', 'secret', 'password_rules',
'ssh_key_change_strategy', 'passphrase', 'recipients',
]
extra_kwargs = {**BaseAutomationSerializer.Meta.extra_kwargs, **{
'recipients': {'label': _('Recipient'), 'help_text': _(
"Currently only mail sending is supported"
)},
}}
def validate_password_rules(self, password_rules):
secret_type = self.initial_secret_type
if secret_type != SecretType.PASSWORD:
return password_rules
length = password_rules.get('length')
symbol_set = password_rules.get('symbol_set', '')
try:
length = int(length)
except Exception as e:
logger.error(e)
msg = _("* Please enter the correct password length")
raise serializers.ValidationError(msg)
if length < 6 or length > 30:
msg = _('* Password length range 6-30 bits')
raise serializers.ValidationError(msg)
if not isinstance(symbol_set, str):
symbol_set = str(symbol_set)
password_rules = {'length': length, 'symbol_set': ''.join(symbol_set)}
return password_rules
def validate(self, attrs):
secret_type = attrs.get('secret_type')
secret_strategy = attrs.get('secret_strategy')
if secret_type == SecretType.PASSWORD:
attrs.pop('ssh_key_change_strategy', None)
if secret_strategy == SecretStrategy.custom:
attrs.pop('password_rules', None)
else:
attrs.pop('secret', None)
elif secret_type == SecretType.SSH_KEY:
attrs.pop('password_rules', None)
if secret_strategy != SecretStrategy.custom:
attrs.pop('secret', None)
return attrs
class ChangeSecretRecordSerializer(serializers.ModelSerializer):
is_success = serializers.SerializerMethodField(label=_('Is success'))
asset = ObjectRelatedField(queryset=Asset.objects, label=_('Asset'))
account = ObjectRelatedField(queryset=Account.objects, label=_('Account'))
execution = ObjectRelatedField(
queryset=AutomationExecution.objects, label=_('Automation task execution')
)
class Meta:
model = ChangeSecretRecord
fields = [
'id', 'asset', 'account', 'date_started',
'date_finished', 'is_success', 'error', 'execution',
]
read_only_fields = fields
@staticmethod
def get_is_success(obj):
if obj.status == 'success':
return _("Success")
return _("Failed")
class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer):
asset = serializers.SerializerMethodField(label=_('Asset'))
account = serializers.SerializerMethodField(label=_('Account'))
is_success = serializers.SerializerMethodField(label=_('Is success'))
class Meta:
model = ChangeSecretRecord
fields = [
'id', 'asset', 'account', 'old_secret', 'new_secret',
'status', 'error', 'is_success'
]
read_only_fields = fields
@staticmethod
def get_asset(instance):
return str(instance.asset)
@staticmethod
def get_account(instance):
return str(instance.account)
@staticmethod
def get_is_success(obj):
if obj.status == 'success':
return _("Success")
return _("Failed")

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext_lazy as _
from assets.models import GatherAccountsAutomation
from common.utils import get_logger
from .base import BaseAutomationSerializer
logger = get_logger(__file__)
__all__ = [
'GatherAccountAutomationSerializer',
]
class GatherAccountAutomationSerializer(BaseAutomationSerializer):
class Meta:
model = GatherAccountsAutomation
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields + ['executed_amount']
fields = BaseAutomationSerializer.Meta.fields + read_only_fields
extra_kwargs = {**BaseAutomationSerializer.Meta.extra_kwargs, **{
'executed_amount': {'label': _('Executed amount')}
}}

View File

@ -1,68 +1,53 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from io import StringIO
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from common.utils import ssh_pubkey_gen, ssh_private_key_gen, validate_ssh_private_key from assets.const import SecretType
from common.drf.fields import EncryptedField from common.drf.fields import EncryptedField, LabeledChoiceField
from .utils import validate_password_for_ansible from .utils import validate_password_for_ansible, validate_ssh_key
class AuthValidateMixin(serializers.Serializer): class AuthValidateMixin(serializers.Serializer):
password = EncryptedField( secret_type = LabeledChoiceField(
label=_('Password'), required=False, allow_blank=True, allow_null=True, choices=SecretType.choices, required=True, label=_('Secret type')
max_length=1024, validators=[validate_password_for_ansible]
) )
private_key = EncryptedField( secret = EncryptedField(
label=_('SSH private key'), required=False, allow_blank=True, label=_('Secret'), required=False, max_length=40960, allow_blank=True,
allow_null=True, max_length=16384 allow_null=True, write_only=True,
) )
passphrase = serializers.CharField( passphrase = serializers.CharField(
allow_blank=True, allow_null=True, required=False, max_length=512, allow_blank=True, allow_null=True, required=False, max_length=512,
write_only=True, label=_('Key password') write_only=True, label=_('Key password')
) )
def validate_private_key(self, private_key): @property
if not private_key: def initial_secret_type(self):
return secret_type = self.initial_data.get('secret_type')
passphrase = self.initial_data.get('passphrase') return secret_type
passphrase = passphrase if passphrase else None
valid = validate_ssh_private_key(private_key, password=passphrase)
if not valid:
raise serializers.ValidationError(_("private key invalid or passphrase error"))
private_key = ssh_private_key_gen(private_key, password=passphrase) def validate_secret(self, secret):
string_io = StringIO() if not secret:
private_key.write_private_key(string_io) return
private_key = string_io.getvalue() secret_type = self.initial_secret_type
return private_key if secret_type == SecretType.PASSWORD:
validate_password_for_ansible(secret)
return secret
elif secret_type == SecretType.SSH_KEY:
passphrase = self.initial_data.get('passphrase')
passphrase = passphrase if passphrase else None
return validate_ssh_key(secret, passphrase)
else:
return secret
@staticmethod @staticmethod
def clean_auth_fields(validated_data): def clean_auth_fields(validated_data):
for field in ('password', 'private_key', 'public_key'): for field in ('secret',):
value = validated_data.get(field) value = validated_data.get(field)
if not value: if not value:
validated_data.pop(field, None) validated_data.pop(field, None)
validated_data.pop('passphrase', None) validated_data.pop('passphrase', None)
@staticmethod
def _validate_gen_key(attrs):
private_key = attrs.get('private_key')
if not private_key:
return attrs
password = attrs.get('passphrase')
username = attrs.get('username')
public_key = ssh_pubkey_gen(private_key, password=password, username=username)
attrs['public_key'] = public_key
return attrs
def validate(self, attrs):
attrs = self._validate_gen_key(attrs)
return super().validate(attrs)
def create(self, validated_data): def create(self, validated_data):
self.clean_auth_fields(validated_data) self.clean_auth_fields(validated_data)
return super().create(validated_data) return super().create(validated_data)

View File

@ -1,13 +1,15 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import OrgResourceModelSerializerMixin from orgs.mixins.serializers import OrgResourceModelSerializerMixin
from ..models import GatheredUser from common.drf.fields import ObjectRelatedField
from ..models import GatheredUser, Asset
class GatheredUserSerializer(OrgResourceModelSerializerMixin): class GatheredUserSerializer(OrgResourceModelSerializerMixin):
asset = ObjectRelatedField(queryset=Asset.objects, label=_('Asset'))
class Meta: class Meta:
model = GatheredUser model = GatheredUser
fields_mini = ['id'] fields_mini = ['id']

View File

@ -1,6 +1,10 @@
from io import StringIO
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from common.utils import ssh_private_key_gen, validate_ssh_private_key
def validate_password_for_ansible(password): def validate_password_for_ansible(password):
""" 校验 Ansible 不支持的特殊字符 """ """ 校验 Ansible 不支持的特殊字符 """
@ -15,3 +19,14 @@ def validate_password_for_ansible(password):
if '"' in password: if '"' in password:
raise serializers.ValidationError(_('Password can not contains `"` ')) raise serializers.ValidationError(_('Password can not contains `"` '))
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

View File

@ -9,3 +9,4 @@ from .gather_facts import *
from .nodes_amount import * from .nodes_amount import *
from .push_account import * from .push_account import *
from .verify_account import * from .verify_account import *
from .gather_accounts import *

View File

@ -7,9 +7,9 @@ logger = get_logger(__file__)
@shared_task(queue='ansible') @shared_task(queue='ansible')
def execute_automation(pid, trigger, mode): def execute_automation(pid, trigger, model):
with tmp_to_root_org(): with tmp_to_root_org():
instance = get_object_or_none(mode, pk=pid) instance = get_object_or_none(model, pk=pid)
if not instance: if not instance:
logger.error("No automation task found: {}".format(pid)) logger.error("No automation task found: {}".format(pid))
return return

View File

@ -0,0 +1,34 @@
# ~*~ coding: utf-8 ~*~
from celery import shared_task
from django.utils.translation import gettext_noop
from orgs.utils import tmp_to_root_org, org_aware_func
from common.utils import get_logger
from assets.models import Node
__all__ = ['gather_asset_accounts']
logger = get_logger(__name__)
@org_aware_func("nodes")
def gather_asset_accounts_util(nodes, task_name):
from assets.models import GatherAccountsAutomation
task_name = GatherAccountsAutomation.generate_unique_name(task_name)
data = {
'name': task_name,
'comment': ', '.join([str(i) for i in nodes])
}
instance = GatherAccountsAutomation.objects.create(**data)
instance.nodes.add(*nodes)
instance.execute()
@shared_task(queue="ansible")
def gather_asset_accounts(node_ids, task_name=None):
if task_name is None:
task_name = gettext_noop("Gather assets accounts")
with tmp_to_root_org():
nodes = Node.objects.filter(id__in=node_ids)
gather_asset_accounts_util(nodes=nodes, task_name=task_name)

View File

@ -27,17 +27,26 @@ router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset')
router.register(r'account-backup-plans', api.AccountBackupPlanViewSet, 'account-backup') router.register(r'account-backup-plans', api.AccountBackupPlanViewSet, 'account-backup')
router.register(r'account-backup-plan-executions', api.AccountBackupPlanExecutionViewSet, 'account-backup-execution') router.register(r'account-backup-plan-executions', api.AccountBackupPlanExecutionViewSet, 'account-backup-execution')
router.register(r'change-secret-automations', api.ChangeSecretAutomationViewSet, 'change-secret-automation')
router.register(r'automation-executions', api.AutomationExecutionViewSet, 'automation-execution')
router.register(r'change-secret-records', api.ChangeSecretRecordViewSet, 'change-secret-record')
router.register(r'gather-account-automations', api.GatherAccountsAutomationViewSet, 'gather-account-automation')
urlpatterns = [ urlpatterns = [
# path('assets/<uuid:pk>/gateways/', api.AssetGatewayListApi.as_view(), name='asset-gateway-list'), # path('assets/<uuid:pk>/gateways/', api.AssetGatewayListApi.as_view(), name='asset-gateway-list'),
path('assets/<uuid:pk>/tasks/', api.AssetTaskCreateApi.as_view(), name='asset-task-create'), path('assets/<uuid:pk>/tasks/', api.AssetTaskCreateApi.as_view(), name='asset-task-create'),
path('assets/tasks/', api.AssetsTaskCreateApi.as_view(), name='assets-task-create'), path('assets/tasks/', api.AssetsTaskCreateApi.as_view(), name='assets-task-create'),
path('assets/<uuid:pk>/perm-users/', api.AssetPermUserListApi.as_view(), name='asset-perm-user-list'), path('assets/<uuid:pk>/perm-users/', api.AssetPermUserListApi.as_view(), name='asset-perm-user-list'),
path('assets/<uuid:pk>/perm-users/<uuid:perm_user_id>/permissions/', api.AssetPermUserPermissionsListApi.as_view(), name='asset-perm-user-permission-list'), path('assets/<uuid:pk>/perm-users/<uuid:perm_user_id>/permissions/', api.AssetPermUserPermissionsListApi.as_view(),
path('assets/<uuid:pk>/perm-user-groups/', api.AssetPermUserGroupListApi.as_view(), name='asset-perm-user-group-list'), name='asset-perm-user-permission-list'),
path('assets/<uuid:pk>/perm-user-groups/<uuid:perm_user_group_id>/permissions/', api.AssetPermUserGroupPermissionsListApi.as_view(), name='asset-perm-user-group-permission-list'), path('assets/<uuid:pk>/perm-user-groups/', api.AssetPermUserGroupListApi.as_view(),
name='asset-perm-user-group-list'),
path('assets/<uuid:pk>/perm-user-groups/<uuid:perm_user_group_id>/permissions/',
api.AssetPermUserGroupPermissionsListApi.as_view(), name='asset-perm-user-group-permission-list'),
path('accounts/tasks/', api.AccountTaskCreateAPI.as_view(), name='account-task-create'), path('accounts/tasks/', api.AccountTaskCreateAPI.as_view(), name='account-task-create'),
path('account-secrets/<uuid:pk>/histories/', api.AccountHistoriesSecretAPI.as_view(), name='account-secret-history'), path('account-secrets/<uuid:pk>/histories/', api.AccountHistoriesSecretAPI.as_view(),
name='account-secret-history'),
path('nodes/category/tree/', api.CategoryTreeApi.as_view(), name='asset-category-tree'), path('nodes/category/tree/', api.CategoryTreeApi.as_view(), name='asset-category-tree'),
path('nodes/tree/', api.NodeListAsTreeApi.as_view(), name='node-tree'), path('nodes/tree/', api.NodeListAsTreeApi.as_view(), name='node-tree'),
@ -52,7 +61,11 @@ urlpatterns = [
path('nodes/<uuid:pk>/tasks/', api.NodeTaskCreateApi.as_view(), name='node-task-create'), path('nodes/<uuid:pk>/tasks/', api.NodeTaskCreateApi.as_view(), name='node-task-create'),
path('gateways/<uuid:pk>/test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'), path('gateways/<uuid:pk>/test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
path('automation/<uuid:pk>/asset/remove/', api.AutomationRemoveAssetApi.as_view(), name='automation-remove-asset'),
path('automation/<uuid:pk>/asset/add/', api.AutomationAddAssetApi.as_view(), name='automation-add-asset'),
path('automation/<uuid:pk>/nodes/', api.AutomationNodeAddRemoveApi.as_view(), name='automation-add-or-remove-node'),
path('automation/<uuid:pk>/assets/', api.AutomationAssetsListApi.as_view(), name='automation-assets'),
] ]
urlpatterns += router.urls urlpatterns += router.urls

View File

@ -178,8 +178,6 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
get_object: callable get_object: callable
get_serializer: callable get_serializer: callable
perform_create: callable perform_create: callable
check_token_permission: callable
create_connection_token: callable
@action(methods=['POST'], detail=False, url_path='secret-info/detail') @action(methods=['POST'], detail=False, url_path='secret-info/detail')
def get_secret_detail(self, request, *args, **kwargs): def get_secret_detail(self, request, *args, **kwargs):
@ -277,10 +275,10 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
from perms.utils.account import PermAccountUtil from perms.utils.account import PermAccountUtil
actions, expire_at = PermAccountUtil().validate_permission(user, asset, account_username) actions, expire_at = PermAccountUtil().validate_permission(user, asset, account_username)
if not actions: if not actions:
error = '' error = 'No actions'
raise PermissionDenied(error) raise PermissionDenied(error)
if expire_at < time.time(): if expire_at < time.time():
error = '' error = 'Expired'
raise PermissionDenied(error) raise PermissionDenied(error)

View File

@ -1,4 +1,5 @@
import base64 import base64
import time
from django.shortcuts import redirect, reverse, render from django.shortcuts import redirect, reverse, render
from django.utils.deprecation import MiddlewareMixin from django.utils.deprecation import MiddlewareMixin

View File

@ -85,7 +85,7 @@ class ConnectionToken(OrgModelMixin, JMSBaseModel):
is_valid = False is_valid = False
error = _('No user or invalid user') error = _('No user or invalid user')
return is_valid, error return is_valid, error
if not self.asset or 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

View File

@ -159,7 +159,7 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
domain = ConnectionTokenDomainSerializer(read_only=True) domain = ConnectionTokenDomainSerializer(read_only=True)
cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True) cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True)
actions = ActionsField() actions = ActionsField()
expired_at = serializers.IntegerField() expire_at = serializers.IntegerField()
class Meta: class Meta:
model = ConnectionToken model = ConnectionToken
@ -167,5 +167,5 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
'id', 'secret', 'id', 'secret',
'user', 'asset', 'account_username', 'account', 'protocol', 'user', 'asset', 'account_username', 'account', 'protocol',
'domain', 'gateway', 'cmd_filter_rules', 'domain', 'gateway', 'cmd_filter_rules',
'actions', 'expired_at', 'actions', 'expire_at',
] ]

View File

@ -44,7 +44,9 @@ class BaseService(object):
if self.is_running: if self.is_running:
msg = f'{self.name} is running: {self.pid}.' msg = f'{self.name} is running: {self.pid}.'
else: else:
msg = f'{self.name} is stopped.' msg = '\033[31m{} is stopped.\033[0m\nYou can manual start it to find the error: \n' \
' $ cd {}\n' \
' $ {}'.format(self.name, self.cwd, ' '.join(self.cmd))
print(msg) print(msg)
# -- log -- # -- log --

View File

@ -76,7 +76,6 @@ class ServicesUtil(object):
def clean_up(self): def clean_up(self):
if not self.EXIT_EVENT.is_set(): if not self.EXIT_EVENT.is_set():
self.EXIT_EVENT.set() self.EXIT_EVENT.set()
self.stop() self.stop()
def show_status(self): def show_status(self):

View File

@ -16,14 +16,13 @@ class IsValidUser(permissions.IsAuthenticated, permissions.BasePermission):
"""Allows access to valid user, is active and not expired""" """Allows access to valid user, is active and not expired"""
def has_permission(self, request, view): def has_permission(self, request, view):
return super(IsValidUser, self).has_permission(request, view) \ return super().has_permission(request, view) \
and request.user.is_valid and request.user.is_valid
class IsValidUserOrConnectionToken(IsValidUser): class IsValidUserOrConnectionToken(IsValidUser):
def has_permission(self, request, view): def has_permission(self, request, view):
return super(IsValidUserOrConnectionToken, self).has_permission(request, view) \ return super().has_permission(request, view) \
or self.is_valid_connection_token(request) or self.is_valid_connection_token(request)
@staticmethod @staticmethod
@ -42,6 +41,12 @@ class OnlySuperUser(IsValidUser):
and request.user.is_superuser and request.user.is_superuser
class IsServiceAccount(IsValidUser):
def has_permission(self, request, view):
return super().has_permission(request, view) \
and request.user.is_service_account
class WithBootstrapToken(permissions.BasePermission): class WithBootstrapToken(permissions.BasePermission):
def has_permission(self, request, view): def has_permission(self, request, view):
authorization = request.META.get('HTTP_AUTHORIZATION', '') authorization = request.META.get('HTTP_AUTHORIZATION', '')

View File

@ -308,14 +308,14 @@ class HealthCheckView(HealthApiMixin):
def get_db_status(): def get_db_status():
t1 = time.time() t1 = time.time()
try: try:
User.objects.first() ok = User.objects.first() is not None
t2 = time.time() t2 = time.time()
return True, t2 - t1 return ok, t2 - t1
except: except Exception as e:
t2 = time.time() return False, str(e)
return False, t2 - t1
def get_redis_status(self): @staticmethod
def get_redis_status():
key = 'HEALTH_CHECK' key = 'HEALTH_CHECK'
t1 = time.time() t1 = time.time()
@ -324,12 +324,12 @@ class HealthCheckView(HealthApiMixin):
cache.set(key, '1', 10) cache.set(key, '1', 10)
got = cache.get(key) got = cache.get(key)
t2 = time.time() t2 = time.time()
if value == got: if value == got:
return True, t2 -t1 return True, t2 - t1
return False, t2 -t1 return False, 'Value not match'
except: except Exception as e:
t2 = time.time() return False, str(e)
return False, t2 - t1
def get(self, request): def get(self, request):
redis_status, redis_time = self.get_redis_status() redis_status, redis_time = self.get_redis_status()
@ -341,7 +341,7 @@ class HealthCheckView(HealthApiMixin):
'db_time': db_time, 'db_time': db_time,
'redis_status': redis_status, 'redis_status': redis_status,
'redis_time': redis_time, 'redis_time': redis_time,
'time': int(time.time()) 'time': int(time.time()),
} }
return Response(data) return Response(data)

View File

@ -3,6 +3,9 @@
import os import os
import re import re
import pytz import pytz
import time
import json
from django.utils import timezone from django.utils import timezone
from django.shortcuts import HttpResponse from django.shortcuts import HttpResponse
from django.conf import settings from django.conf import settings
@ -92,3 +95,37 @@ class RefererCheckMiddleware:
return HttpResponseForbidden('CSRF CHECK ERROR') return HttpResponseForbidden('CSRF CHECK ERROR')
response = self.get_response(request) response = self.get_response(request)
return response return response
class StartMiddleware:
def __init__(self, get_response):
self.get_response = get_response
if not settings.DEBUG_DEV:
raise MiddlewareNotUsed
def __call__(self, request):
request._s_time_start = time.time()
response = self.get_response(request)
request._s_time_end = time.time()
if request.path == '/api/health/':
data = response.data
data['pre_middleware_time'] = request._e_time_start - request._s_time_start
data['api_time'] = request._e_time_end - request._e_time_start
data['post_middleware_time'] = request._s_time_end - request._e_time_end
response.content = json.dumps(data)
response.headers['Content-Length'] = str(len(response.content))
return response
return response
class EndMiddleware:
def __init__(self, get_response):
self.get_response = get_response
if not settings.DEBUG_DEV:
raise MiddlewareNotUsed
def __call__(self, request):
request._e_time_start = time.time()
response = self.get_response(request)
request._e_time_end = time.time()
return response

View File

@ -86,6 +86,7 @@ INSTALLED_APPS = [
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'jumpserver.middleware.StartMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware', 'django.middleware.locale.LocaleMiddleware',
@ -105,6 +106,7 @@ MIDDLEWARE = [
'authentication.middleware.ThirdPartyLoginMiddleware', 'authentication.middleware.ThirdPartyLoginMiddleware',
'authentication.middleware.SessionCookieMiddleware', 'authentication.middleware.SessionCookieMiddleware',
'simple_history.middleware.HistoryRequestMiddleware', 'simple_history.middleware.HistoryRequestMiddleware',
'jumpserver.middleware.EndMiddleware',
] ]
ROOT_URLCONF = 'jumpserver.urls' ROOT_URLCONF = 'jumpserver.urls'

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0b396cc9a485f6474d14ca30a1a7ba4f954b07754148b964efbb21519c55b280 oid sha256:314c29cb8b10aaddbb030bf49af293be23f0153ff1f1c7562946879574ce6de8
size 102849 size 102801

View File

@ -858,7 +858,7 @@ msgstr "校验日期"
#: assets/models/base.py:63 #: assets/models/base.py:63
msgid "Privileged" msgid "Privileged"
msgstr "特权" msgstr "特权账号"
#: assets/models/cmd_filter.py:32 perms/models/asset_permission.py:61 #: assets/models/cmd_filter.py:32 perms/models/asset_permission.py:61
#: users/models/group.py:31 users/models/user.py:671 #: users/models/group.py:31 users/models/user.py:671

View File

@ -144,7 +144,7 @@ def check_server_performance_period():
ServerPerformanceCheckUtil().check_and_publish() ServerPerformanceCheckUtil().check_and_publish()
@shared_task(queue="ansible", verbose_name=_("Hello"), comment="an test shared task") @shared_task(verbose_name=_("Hello"), comment="an test shared task")
def hello(name, callback=None): def hello(name, callback=None):
from users.models import User from users.models import User
import time import time

View File

@ -1,17 +1,16 @@
import uuid import uuid
import logging import logging
from functools import reduce
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db import models from django.db import models
from django.db.models import F, Q, TextChoices from django.db.models import F, Q, TextChoices
from collections import defaultdict
from common.utils import lazyproperty, date_expired_default
from common.db.models import BaseCreateUpdateModel, UnionQuerySet
from assets.models import Asset, Node, FamilyMixin, Account from assets.models import Asset, Node, FamilyMixin, Account
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
from orgs.mixins.models import OrgManager from orgs.mixins.models import OrgManager
from common.utils import lazyproperty, date_expired_default
from common.db.models import BaseCreateUpdateModel, UnionQuerySet
from .const import Action, SpecialAccount from .const import Action, SpecialAccount
__all__ = [ __all__ = [

View File

@ -6,10 +6,11 @@ from rest_framework.fields import empty
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db.models import Q from django.db.models import Q
from common.drf.fields import ObjectRelatedField
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from assets.models import Asset, Node from assets.models import Asset, Node
from users.models import User, UserGroup from users.models import User, UserGroup
from perms.models import AssetPermission, Action from perms.models import AssetPermission, Action
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
__all__ = ['AssetPermissionSerializer', 'ActionsField'] __all__ = ['AssetPermissionSerializer', 'ActionsField']
@ -44,18 +45,10 @@ class ActionsDisplayField(ActionsField):
class AssetPermissionSerializer(BulkOrgResourceModelSerializer): class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
users_display = serializers.ListField( users = ObjectRelatedField(queryset=User.objects, many=True, required=False)
child=serializers.CharField(), label=_('Users display'), required=False user_groups = ObjectRelatedField(queryset=UserGroup.objects, many=True, required=False)
) assets = ObjectRelatedField(queryset=Asset.objects, many=True, required=False)
user_groups_display = serializers.ListField( nodes = ObjectRelatedField(queryset=Node.objects, many=True, required=False)
child=serializers.CharField(), label=_('User groups display'), required=False
)
assets_display = serializers.ListField(
child=serializers.CharField(), label=_('Assets display'), required=False
)
nodes_display = serializers.ListField(
child=serializers.CharField(), label=_('Nodes display'), required=False
)
actions = ActionsField(required=False, allow_null=True, label=_("Actions")) actions = ActionsField(required=False, allow_null=True, label=_("Actions"))
is_valid = serializers.BooleanField(read_only=True, label=_("Is valid")) is_valid = serializers.BooleanField(read_only=True, label=_("Is valid"))
is_expired = serializers.BooleanField(read_only=True, label=_('Is expired')) is_expired = serializers.BooleanField(read_only=True, label=_('Is expired'))
@ -64,24 +57,16 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
model = AssetPermission model = AssetPermission
fields_mini = ['id', 'name'] fields_mini = ['id', 'name']
fields_small = fields_mini + [ fields_small = fields_mini + [
'is_active', 'is_expired', 'is_valid', 'actions', 'accounts', 'is_active', 'is_expired', 'is_valid',
'accounts', 'actions', 'created_by', 'date_created', 'date_expired',
'created_by', 'date_created', 'date_expired',
'date_start', 'comment', 'from_ticket' 'date_start', 'comment', 'from_ticket'
] ]
fields_m2m = [ fields_m2m = [
'users', 'users_display', 'user_groups', 'user_groups_display', 'assets', 'users', 'user_groups', 'assets', 'nodes',
'assets_display', 'nodes', 'nodes_display',
'users_amount', 'user_groups_amount', 'assets_amount',
'nodes_amount',
] ]
fields = fields_small + fields_m2m fields = fields_small + fields_m2m
read_only_fields = ['created_by', 'date_created', 'from_ticket'] read_only_fields = ['created_by', 'date_created', 'from_ticket']
extra_kwargs = { extra_kwargs = {
'users_amount': {'label': _('Users amount')},
'user_groups_amount': {'label': _('User groups amount')},
'assets_amount': {'label': _('Assets amount')},
'nodes_amount': {'label': _('Nodes amount')},
'actions': {'label': _('Actions')}, 'actions': {'label': _('Actions')},
'is_expired': {'label': _('Is expired')}, 'is_expired': {'label': _('Is expired')},
'is_valid': {'label': _('Is valid')}, 'is_valid': {'label': _('Is valid')},

View File

@ -53,7 +53,9 @@ class PermAccountUtil(AssetPermissionUtil):
user, asset, with_actions=True, with_perms=True user, asset, with_actions=True, with_perms=True
) )
perm = perms.first() perm = perms.first()
account = accounts.filter(username=account_username).first() actions = []
actions = account.actions if account else [] for account in accounts:
expire_at = perm.date_expired if perm else time.time() if account.username == account_username:
actions = account.actions
expire_at = perm.date_expired.timestamp() if perm else time.time()
return actions, expire_at return actions, expire_at

View File

@ -1,2 +1,3 @@
from .applet import * from .applet import *
from .host import * from .host import *
from .relation import *

View File

@ -2,10 +2,14 @@ from rest_framework import viewsets
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from common.permissions import IsServiceAccount
from common.drf.api import JMSModelViewSet from common.drf.api import JMSModelViewSet
from orgs.utils import tmp_to_builtin_org from orgs.utils import tmp_to_builtin_org
from terminal import serializers from terminal.serializers import (
from terminal.models import AppletHost, Applet, AppletHostDeployment AppletHostSerializer, AppletHostDeploymentSerializer,
AppletHostStartupSerializer
)
from terminal.models import AppletHost, AppletHostDeployment
from terminal.tasks import run_applet_host_deployment from terminal.tasks import run_applet_host_deployment
@ -13,37 +17,29 @@ __all__ = ['AppletHostViewSet', 'AppletHostDeploymentViewSet']
class AppletHostViewSet(JMSModelViewSet): class AppletHostViewSet(JMSModelViewSet):
serializer_class = serializers.AppletHostSerializer serializer_class = AppletHostSerializer
queryset = AppletHost.objects.all() queryset = AppletHost.objects.all()
rbac_perms = {
'accounts': 'terminal.view_applethost',
'reports': '*'
}
@action(methods=['post'], detail=True, serializer_class=serializers.AppletHostReportSerializer) def dispatch(self, request, *args, **kwargs):
def reports(self, request, *args, **kwargs): with tmp_to_builtin_org(system=1):
# 1. Host 和 Terminal 关联 return super().dispatch(request, *args, **kwargs)
# 2. 上报 安装的 Applets 每小时
def get_permissions(self):
if self.action == 'startup':
return [IsServiceAccount()]
return super().get_permissions()
@action(methods=['post'], detail=True, serializer_class=AppletHostStartupSerializer)
def startup(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
serializer = self.get_serializer(data=request.data) serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
data = serializer.validated_data
instance.check_terminal_binding(request) instance.check_terminal_binding(request)
instance.check_applets_state(data['applets'])
return Response({'msg': 'ok'}) return Response({'msg': 'ok'})
@action(methods=['get'], detail=True, serializer_class=serializers.AppletHostAccountSerializer)
def accounts(self, request, *args, **kwargs):
host = self.get_object()
with tmp_to_builtin_org(system=1):
accounts = host.accounts.all().filter(privileged=False)
response = self.get_paginated_response_from_queryset(accounts)
return response
class AppletHostDeploymentViewSet(viewsets.ModelViewSet): class AppletHostDeploymentViewSet(viewsets.ModelViewSet):
serializer_class = serializers.AppletHostDeploymentSerializer serializer_class = AppletHostDeploymentSerializer
queryset = AppletHostDeployment.objects.all() queryset = AppletHostDeployment.objects.all()
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):

View File

@ -0,0 +1,86 @@
from typing import Callable
from django.shortcuts import get_object_or_404
from django.conf import settings
from rest_framework.request import Request
from rest_framework.decorators import action
from rest_framework.response import Response
from common.drf.api import JMSModelViewSet
from common.permissions import IsServiceAccount
from common.utils import is_uuid
from orgs.utils import tmp_to_builtin_org
from rbac.permissions import RBACPermission
from terminal.models import AppletHost
from terminal.serializers import (
AppletHostAccountSerializer,
AppletPublicationSerializer,
AppletHostAppletReportSerializer,
)
class HostMixin:
request: Request
permission_denied: Callable
kwargs: dict
rbac_perms = (
('list', 'terminal.view_applethost'),
('retrieve', 'terminal.view_applethost'),
)
def get_permissions(self):
if self.kwargs.get('host') and settings.DEBUG:
return [RBACPermission()]
else:
return [IsServiceAccount()]
def self_host(self):
try:
return self.request.user.terminal.applet_host
except AttributeError:
raise self.permission_denied(self.request, 'User has no applet host')
def pk_host(self):
return get_object_or_404(AppletHost, id=self.kwargs.get('host'))
@property
def host(self):
if self.kwargs.get('host'):
return self.pk_host()
else:
return self.self_host()
class AppletHostAccountsViewSet(HostMixin, JMSModelViewSet):
serializer_class = AppletHostAccountSerializer
def get_queryset(self):
with tmp_to_builtin_org(system=1):
queryset = self.host.accounts.all()
return queryset
class AppletHostAppletViewSet(HostMixin, JMSModelViewSet):
host: AppletHost
serializer_class = AppletPublicationSerializer
def get_object(self):
pk = self.kwargs.get('pk')
if not is_uuid(pk):
return self.host.publications.get(applet__name=pk)
else:
return self.host.publications.get(pk=pk)
def get_queryset(self):
queryset = self.host.publications.all()
return queryset
@action(methods=['post'], detail=False)
def reports(self, request, *args, **kwargs):
serializer = AppletHostAppletReportSerializer(data=request.data, many=True)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
self.host.check_applets_state(data)
publications = self.host.publications.all()
serializer = AppletPublicationSerializer(publications, many=True)
return Response(serializer.data)

View File

@ -14,7 +14,7 @@
RDS_fSingleSessionPerUser: 1 RDS_fSingleSessionPerUser: 1
RDS_MaxDisconnectionTime: 60000 RDS_MaxDisconnectionTime: 60000
RDS_RemoteAppLogoffTimeLimit: 0 RDS_RemoteAppLogoffTimeLimit: 0
TinkerInstaller: JumpServer-Remoteapp_v0.0.1.exe TinkerInstaller: Tinker_Installer_v0.0.1.exe
tasks: tasks:
- name: Install RDS-Licensing (RDS) - name: Install RDS-Licensing (RDS)
@ -31,12 +31,12 @@
include_management_tools: yes include_management_tools: yes
register: rds_install register: rds_install
- name: Download JumpServer Remoteapp installer (jumpserver) - name: Download JumpServer Tinker installer (jumpserver)
ansible.windows.win_get_url: ansible.windows.win_get_url:
url: "{{ DownloadHost }}/{{ TinkerInstaller }}" url: "{{ DownloadHost }}/{{ TinkerInstaller }}"
dest: "{{ ansible_env.TEMP }}\\{{ TinkerInstaller }}" dest: "{{ ansible_env.TEMP }}\\{{ TinkerInstaller }}"
- name: Install JumpServer Remoteapp agent (jumpserver) - name: Install JumpServer Tinker (jumpserver)
ansible.windows.win_package: ansible.windows.win_package:
path: "{{ ansible_env.TEMP }}\\{{ TinkerInstaller }}" path: "{{ ansible_env.TEMP }}\\{{ TinkerInstaller }}"
arguments: arguments:
@ -48,7 +48,7 @@
- name: Set remote-server on the global system path (remote-server) - name: Set remote-server on the global system path (remote-server)
ansible.windows.win_path: ansible.windows.win_path:
elements: elements:
- '%USERPROFILE%\AppData\Local\Programs\JumpServer-Remoteapp\' - '%USERPROFILE%\AppData\Local\Programs\Tinker\'
scope: user scope: user
- name: Download python-3.10.8 - name: Download python-3.10.8
@ -153,18 +153,18 @@
arguments: arguments:
- /quiet - /quiet
- name: Generate component config - name: Generate tinkerd component config
ansible.windows.win_shell: ansible.windows.win_shell:
"remoteapp-server config --hostname {{ HOST_NAME }} --core_host {{ CORE_HOST }} "tinkerd config --hostname {{ HOST_NAME }} --core_host {{ CORE_HOST }}
--token {{ BOOTSTRAP_TOKEN }} --host_id {{ HOST_ID }}" --token {{ BOOTSTRAP_TOKEN }} --host_id {{ HOST_ID }}"
- name: Install remoteapp-server service - name: Install tinkerd service
ansible.windows.win_shell: ansible.windows.win_shell:
"remoteapp-server service install" "tinkerd service install"
- name: Start remoteapp-server service - name: Start tinkerd service
ansible.windows.win_shell: ansible.windows.win_shell:
"remoteapp-server service start" "tinkerd service start"
- name: Wait Tinker api health - name: Wait Tinker api health
ansible.windows.win_uri: ansible.windows.win_uri:

View File

@ -11,7 +11,6 @@ from common.db.models import JMSBaseModel
from common.utils import random_string from common.utils import random_string
from assets.models import Host from assets.models import Host
__all__ = ['AppletHost', 'AppletHostDeployment'] __all__ = ['AppletHost', 'AppletHostDeployment']
@ -26,7 +25,7 @@ class AppletHost(Host):
) )
applets = models.ManyToManyField( applets = models.ManyToManyField(
'Applet', verbose_name=_('Applet'), 'Applet', verbose_name=_('Applet'),
through='AppletPublication', through_fields=('host', 'applet'), through='AppletPublication', through_fields=('host', 'applet'),
) )
LOCKING_ORG = '00000000-0000-0000-0000-000000000004' LOCKING_ORG = '00000000-0000-0000-0000-000000000004'
@ -34,10 +33,10 @@ class AppletHost(Host):
return self.name return self.name
@property @property
def status(self): def load(self):
if self.terminal: if not self.terminal:
return 'online' return 'offline'
return self.terminal.status return self.terminal.load
def check_terminal_binding(self, request): def check_terminal_binding(self, request):
request_terminal = getattr(request.user, 'terminal', None) request_terminal = getattr(request.user, 'terminal', None)
@ -70,8 +69,8 @@ class AppletHost(Host):
status_applets['published'].append(applet) status_applets['published'].append(applet)
for status, applets in status_applets.items(): for status, applets in status_applets.items():
self.publications.filter(applet__in=applets)\ self.publications.filter(applet__in=applets) \
.exclude(status=status)\ .exclude(status=status) \
.update(status=status) .update(status=status)
@staticmethod @staticmethod
@ -95,7 +94,7 @@ class AppletHost(Host):
account = account_model( account = account_model(
username=username, secret=password, name=username, username=username, secret=password, name=username,
asset_id=self.id, secret_type='password', version=1, asset_id=self.id, secret_type='password', version=1,
org_id=self.LOCKING_ORG org_id=self.LOCKING_ORG, is_active=False,
) )
accounts.append(account) accounts.append(account)
bulk_create_with_history(accounts, account_model, batch_size=20) bulk_create_with_history(accounts, account_model, batch_size=20)

View File

@ -1,4 +1,5 @@
import uuid import uuid
import time
from django.utils import timezone from django.utils import timezone
from django.db import models from django.db import models
@ -34,7 +35,7 @@ class TerminalStatusMixin:
def is_alive(self): def is_alive(self):
if not self.last_stat: if not self.last_stat:
return False return False
return self.last_stat.date_created > timezone.now() - timezone.timedelta(seconds=120) return time.time() - self.last_stat.date_created.timestamp() < 150
class StorageMixin: class StorageMixin:

View File

@ -17,7 +17,7 @@ class AppletPublicationSerializer(serializers.ModelSerializer):
UNPUBLISHED = 'unpublished', _('Unpublished') UNPUBLISHED = 'unpublished', _('Unpublished')
NOT_MATCH = 'not_match', _('Not match') NOT_MATCH = 'not_match', _('Not match')
applet = ObjectRelatedField(attrs=('id', 'display_name', 'icon', 'version'), queryset=Applet.objects.all()) applet = ObjectRelatedField(attrs=('id', 'name', 'display_name', 'icon', 'version'), queryset=Applet.objects.all())
host = ObjectRelatedField(queryset=AppletHost.objects.all()) host = ObjectRelatedField(queryset=AppletHost.objects.all())
status = LabeledChoiceField(choices=Status.choices, label=_("Status")) status = LabeledChoiceField(choices=Status.choices, label=_("Status"))

View File

@ -2,16 +2,18 @@ from rest_framework import serializers
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from common.validators import ProjectUniqueValidator from common.validators import ProjectUniqueValidator
from common.drf.fields import ObjectRelatedField from common.drf.fields import ObjectRelatedField, LabeledChoiceField
from assets.models import Platform, Account from assets.models import Platform, Account
from assets.serializers import HostSerializer from assets.serializers import HostSerializer
from ..models import AppletHost, AppletHostDeployment, Applet from ..models import AppletHost, AppletHostDeployment, Applet
from .applet import AppletSerializer from .applet import AppletSerializer
from .. import const
__all__ = [ __all__ = [
'AppletHostSerializer', 'AppletHostDeploymentSerializer', 'AppletHostSerializer', 'AppletHostDeploymentSerializer',
'AppletHostAccountSerializer', 'AppletHostReportSerializer' 'AppletHostAccountSerializer', 'AppletHostAppletReportSerializer',
'AppletHostStartupSerializer',
] ]
@ -34,14 +36,16 @@ class DeployOptionsSerializer(serializers.Serializer):
class AppletHostSerializer(HostSerializer): class AppletHostSerializer(HostSerializer):
deploy_options = DeployOptionsSerializer(required=False, label=_("Deploy options")) deploy_options = DeployOptionsSerializer(required=False, label=_("Deploy options"))
load = LabeledChoiceField(
read_only=True, label=_('Load status'), choices=const.ComponentLoad.choices,
)
class Meta(HostSerializer.Meta): class Meta(HostSerializer.Meta):
model = AppletHost model = AppletHost
fields = HostSerializer.Meta.fields + [ fields = HostSerializer.Meta.fields + [
'status', 'date_synced', 'deploy_options' 'load', 'date_synced', 'deploy_options'
] ]
extra_kwargs = { extra_kwargs = {
'status': {'read_only': True},
'date_synced': {'read_only': True} 'date_synced': {'read_only': True}
} }
@ -93,8 +97,14 @@ class AppletHostDeploymentSerializer(serializers.ModelSerializer):
class AppletHostAccountSerializer(serializers.ModelSerializer): class AppletHostAccountSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Account model = Account
fields = ['id', 'username', 'secret', 'date_updated'] fields = ['id', 'username', 'secret', 'is_active', 'date_updated']
class AppletHostReportSerializer(serializers.Serializer): class AppletHostAppletReportSerializer(serializers.Serializer):
applets = ObjectRelatedField(attrs=('id', 'name', 'version'), queryset=Applet.objects.all(), many=True) id = serializers.UUIDField(read_only=True)
name = serializers.CharField()
version = serializers.CharField()
class AppletHostStartupSerializer(serializers.Serializer):
pass

View File

@ -4,6 +4,7 @@
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from orgs.utils import tmp_to_builtin_org
from .models import Applet, AppletHost from .models import Applet, AppletHost
@ -13,7 +14,8 @@ def on_applet_host_create(sender, instance, created=False, **kwargs):
return return
applets = Applet.objects.all() applets = Applet.objects.all()
instance.applets.set(applets) instance.applets.set(applets)
instance.generate_accounts() with tmp_to_builtin_org(system=1):
instance.generate_accounts()
@receiver(post_save, sender=Applet) @receiver(post_save, sender=Applet)

View File

@ -59,7 +59,6 @@ class BaseTerminal(object):
try: try:
status = status_serializer.save() status = status_serializer.save()
print("Save status ok: ", status)
time.sleep(self.interval) time.sleep(self.interval)
except OperationalError: except OperationalError:
print("Save status error, close old connections") print("Save status error, close old connections")

View File

@ -12,8 +12,8 @@ app_name = 'terminal'
router = BulkRouter() router = BulkRouter()
router.register(r'sessions', api.SessionViewSet, 'session') router.register(r'sessions', api.SessionViewSet, 'session')
router.register(r'terminals/(?P<terminal>[a-zA-Z0-9\-]{36})?/?status', api.StatusViewSet, 'terminal-status') router.register(r'terminals/((?P<terminal>[^/.]{36})/)?status', api.StatusViewSet, 'terminal-status')
router.register(r'terminals/(?P<terminal>[a-zA-Z0-9\-]{36})?/?sessions', api.SessionViewSet, 'terminal-sessions') router.register(r'terminals/((?P<terminal>[^/.]{36})/)?sessions', api.SessionViewSet, 'terminal-sessions')
router.register(r'terminals', api.TerminalViewSet, 'terminal') router.register(r'terminals', api.TerminalViewSet, 'terminal')
router.register(r'tasks', api.TaskViewSet, 'tasks') router.register(r'tasks', api.TaskViewSet, 'tasks')
router.register(r'commands', api.CommandViewSet, 'command') router.register(r'commands', api.CommandViewSet, 'command')
@ -25,6 +25,8 @@ router.register(r'session-join-records', api.SessionJoinRecordsViewSet, 'session
router.register(r'endpoints', api.EndpointViewSet, 'endpoint') router.register(r'endpoints', api.EndpointViewSet, 'endpoint')
router.register(r'endpoint-rules', api.EndpointRuleViewSet, 'endpoint-rule') router.register(r'endpoint-rules', api.EndpointRuleViewSet, 'endpoint-rule')
router.register(r'applets', api.AppletViewSet, 'applet') router.register(r'applets', api.AppletViewSet, 'applet')
router.register(r'applet-hosts/((?P<host>[^/.]+)/)?accounts', api.AppletHostAccountsViewSet, 'applet-host-account')
router.register(r'applet-hosts/((?P<host>[^/.]+)/)?applets', api.AppletHostAppletViewSet, 'applet-host-applet')
router.register(r'applet-hosts', api.AppletHostViewSet, 'applet-host') router.register(r'applet-hosts', api.AppletHostViewSet, 'applet-host')
router.register(r'applet-publications', api.AppletPublicationViewSet, 'applet-publication') router.register(r'applet-publications', api.AppletPublicationViewSet, 'applet-publication')
router.register(r'applet-host-deployments', api.AppletHostDeploymentViewSet, 'applet-host-deployment') router.register(r'applet-host-deployments', api.AppletHostDeploymentViewSet, 'applet-host-deployment')

View File

@ -5,21 +5,24 @@ from rest_framework import serializers
from orgs.models import Organization from orgs.models import Organization
from orgs.utils import get_current_org_id from orgs.utils import get_current_org_id
from orgs.mixins.serializers import OrgResourceModelSerializerMixin from orgs.mixins.serializers import OrgResourceModelSerializerMixin
from common.drf.fields import LabeledChoiceField
from tickets.models import TicketFlow, ApprovalRule from tickets.models import TicketFlow, ApprovalRule
from tickets.const import TicketApprovalStrategy from tickets.const import TicketApprovalStrategy, TicketType
__all__ = ['TicketFlowSerializer'] __all__ = ['TicketFlowSerializer']
class TicketFlowApproveSerializer(serializers.ModelSerializer): class TicketFlowApproveSerializer(serializers.ModelSerializer):
strategy_display = serializers.ReadOnlyField(source='get_strategy_display', label=_('Approve strategy')) strategy = LabeledChoiceField(
choices=TicketApprovalStrategy.choices, required=True, label=_('Approve strategy')
)
assignees_read_only = serializers.SerializerMethodField(label=_('Assignees')) assignees_read_only = serializers.SerializerMethodField(label=_('Assignees'))
assignees_display = serializers.SerializerMethodField(label=_('Assignees display')) assignees_display = serializers.SerializerMethodField(label=_('Assignees display'))
class Meta: class Meta:
model = ApprovalRule model = ApprovalRule
fields_small = [ fields_small = [
'level', 'strategy', 'assignees_read_only', 'assignees_display', 'strategy_display' 'level', 'strategy', 'assignees_read_only', 'assignees_display',
] ]
fields_m2m = ['assignees', ] fields_m2m = ['assignees', ]
fields = fields_small + fields_m2m fields = fields_small + fields_m2m
@ -46,14 +49,16 @@ class TicketFlowApproveSerializer(serializers.ModelSerializer):
class TicketFlowSerializer(OrgResourceModelSerializerMixin): class TicketFlowSerializer(OrgResourceModelSerializerMixin):
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display')) type = LabeledChoiceField(
choices=TicketType.choices, required=True, label=_('Type')
)
rules = TicketFlowApproveSerializer(many=True, required=True) rules = TicketFlowApproveSerializer(many=True, required=True)
class Meta: class Meta:
model = TicketFlow model = TicketFlow
fields_mini = ['id', ] fields_mini = ['id', ]
fields_small = fields_mini + [ fields_small = fields_mini + [
'type', 'type_display', 'approval_level', 'created_by', 'date_created', 'date_updated', 'type', 'approval_level', 'created_by', 'date_created', 'date_updated',
'org_id', 'org_name' 'org_id', 'org_name'
] ]
fields = fields_small + ['rules', ] fields = fields_small + ['rules', ]