mirror of https://github.com/jumpserver/jumpserver
merge: with merge remote
commit
fbf65f437a
|
@ -4,6 +4,7 @@ from rest_framework import status, viewsets
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
|
from common.const.choices import Trigger
|
||||||
from assets import serializers
|
from assets import serializers
|
||||||
from assets.tasks import execute_account_backup_plan
|
from assets.tasks import execute_account_backup_plan
|
||||||
from assets.models import (
|
from assets.models import (
|
||||||
|
@ -38,9 +39,7 @@ class AccountBackupPlanExecutionViewSet(viewsets.ModelViewSet):
|
||||||
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)
|
||||||
pid = serializer.data.get('plan')
|
pid = serializer.data.get('plan')
|
||||||
task = execute_account_backup_plan.delay(
|
task = execute_account_backup_plan.delay(pid=pid, trigger=Trigger.manual)
|
||||||
pid=pid, trigger=AccountBackupPlanExecution.Trigger.manual
|
|
||||||
)
|
|
||||||
return Response({'task': task.id}, status=status.HTTP_201_CREATED)
|
return Response({'task': task.id}, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
|
|
|
@ -82,15 +82,16 @@ class AssetAccountHandler(BaseAccountHandler):
|
||||||
|
|
||||||
# TODO 可以优化一下查询 在账号上做 category 的缓存 避免数据量大时连表操作
|
# TODO 可以优化一下查询 在账号上做 category 的缓存 避免数据量大时连表操作
|
||||||
qs = Account.objects.filter(
|
qs = Account.objects.filter(
|
||||||
asset__platform__category__in=categories
|
asset__platform__type__in=categories
|
||||||
).annotate(category=F('asset__platform__category'))
|
).annotate(category=F('asset__platform__type'))
|
||||||
|
print(qs, categories)
|
||||||
if not qs.exists():
|
if not qs.exists():
|
||||||
return data_map
|
return data_map
|
||||||
|
|
||||||
category_dict = {}
|
category_dict = {}
|
||||||
for i in AllTypes.grouped_choices_to_objs():
|
for i in AllTypes.grouped_choices_to_objs():
|
||||||
for j in i['children']:
|
for j in i['children']:
|
||||||
category_dict[j['value']] = j['label']
|
category_dict[j['value']] = j['display_name']
|
||||||
|
|
||||||
header_fields = cls.get_header_fields(AccountSecretSerializer(qs.first()))
|
header_fields = cls.get_header_fields(AccountSecretSerializer(qs.first()))
|
||||||
account_category_map = defaultdict(list)
|
account_category_map = defaultdict(list)
|
|
@ -12,7 +12,7 @@ from .handlers import AccountBackupHandler
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AccountBackupExecutionManager:
|
class AccountBackupManager:
|
||||||
def __init__(self, execution):
|
def __init__(self, execution):
|
||||||
self.execution = execution
|
self.execution = execution
|
||||||
self.date_start = timezone.now()
|
self.date_start = timezone.now()
|
|
@ -1,17 +1,28 @@
|
||||||
|
import os
|
||||||
|
import time
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
from openpyxl import Workbook
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from common.utils import lazyproperty, gen_key_pair
|
from common.utils.timezone import local_now_display
|
||||||
|
from common.utils.file import encrypt_and_compress_zip_file
|
||||||
|
from common.utils import get_logger, lazyproperty, gen_key_pair
|
||||||
|
from users.models import User
|
||||||
from assets.models import ChangeSecretRecord
|
from assets.models import ChangeSecretRecord
|
||||||
|
from assets.notifications import ChangeSecretExecutionTaskMsg
|
||||||
|
from assets.serializers import ChangeSecretRecordBackUpSerializer
|
||||||
from assets.const import (
|
from assets.const import (
|
||||||
AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy, DEFAULT_PASSWORD_RULES
|
AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy, DEFAULT_PASSWORD_RULES
|
||||||
)
|
)
|
||||||
from ..base.manager import BasePlaybookManager
|
from ..base.manager import BasePlaybookManager
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ChangeSecretManager(BasePlaybookManager):
|
class ChangeSecretManager(BasePlaybookManager):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -125,7 +136,7 @@ class ChangeSecretManager(BasePlaybookManager):
|
||||||
new_secret = self.get_secret()
|
new_secret = self.get_secret()
|
||||||
|
|
||||||
recorder = ChangeSecretRecord(
|
recorder = ChangeSecretRecord(
|
||||||
account=account, execution=self.execution,
|
asset=asset, account=account, execution=self.execution,
|
||||||
old_secret=account.secret, new_secret=new_secret,
|
old_secret=account.secret, new_secret=new_secret,
|
||||||
)
|
)
|
||||||
records.append(recorder)
|
records.append(recorder)
|
||||||
|
@ -172,4 +183,49 @@ class ChangeSecretManager(BasePlaybookManager):
|
||||||
recorder.save()
|
recorder.save()
|
||||||
|
|
||||||
def on_runner_failed(self, runner, e):
|
def on_runner_failed(self, runner, e):
|
||||||
pass
|
logger.error("Change secret error: ", e)
|
||||||
|
|
||||||
|
def run(self, *args, **kwargs):
|
||||||
|
super().run(*args, **kwargs)
|
||||||
|
recorders = self.name_recorder_mapper.values()
|
||||||
|
recorders = list(recorders)
|
||||||
|
self.send_recorder_mail(recorders)
|
||||||
|
|
||||||
|
def send_recorder_mail(self, recorders):
|
||||||
|
recipients = self.execution.recipients
|
||||||
|
if not recorders or not recipients:
|
||||||
|
return
|
||||||
|
recipients = User.objects.filter(id__in=list(recipients))
|
||||||
|
|
||||||
|
name = self.execution.snapshot['name']
|
||||||
|
path = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp')
|
||||||
|
filename = os.path.join(path, f'{name}-{local_now_display()}-{time.time()}.xlsx')
|
||||||
|
if not self.create_file(recorders, filename):
|
||||||
|
return
|
||||||
|
|
||||||
|
for user in recipients:
|
||||||
|
attachments = []
|
||||||
|
if user.secret_key:
|
||||||
|
password = user.secret_key.encode('utf8')
|
||||||
|
attachment = os.path.join(path, f'{name}-{local_now_display()}-{time.time()}.zip')
|
||||||
|
encrypt_and_compress_zip_file(attachment, password, [filename])
|
||||||
|
attachments = [attachment]
|
||||||
|
ChangeSecretExecutionTaskMsg(name, user).publish(attachments)
|
||||||
|
os.remove(filename)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_file(recorders, filename):
|
||||||
|
serializer_cls = ChangeSecretRecordBackUpSerializer
|
||||||
|
serializer = serializer_cls(recorders, many=True)
|
||||||
|
header = [v.label for v in serializer.child.fields.values()]
|
||||||
|
rows = [list(row.values()) for row in serializer.data]
|
||||||
|
if not rows:
|
||||||
|
return False
|
||||||
|
|
||||||
|
rows.insert(0, header)
|
||||||
|
wb = Workbook(filename)
|
||||||
|
ws = wb.create_sheet('Sheet1')
|
||||||
|
for row in rows:
|
||||||
|
ws.append(row)
|
||||||
|
wb.save(filename)
|
||||||
|
return True
|
||||||
|
|
|
@ -3,6 +3,7 @@ from .gather_facts.manager import GatherFactsManager
|
||||||
from .gather_accounts.manager import GatherAccountsManager
|
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 ..const import AutomationTypes
|
from ..const import AutomationTypes
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,6 +14,8 @@ class ExecutionManager:
|
||||||
AutomationTypes.gather_accounts: GatherAccountsManager,
|
AutomationTypes.gather_accounts: GatherAccountsManager,
|
||||||
AutomationTypes.verify_account: VerifyAccountManager,
|
AutomationTypes.verify_account: VerifyAccountManager,
|
||||||
AutomationTypes.push_account: PushAccountManager,
|
AutomationTypes.push_account: PushAccountManager,
|
||||||
|
# TODO 后期迁移到自动化策略中
|
||||||
|
'backup_account': AccountBackupManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, execution):
|
def __init__(self, execution):
|
||||||
|
@ -21,4 +24,3 @@ class ExecutionManager:
|
||||||
|
|
||||||
def run(self, *args, **kwargs):
|
def run(self, *args, **kwargs):
|
||||||
return self._runner.run(*args, **kwargs)
|
return self._runner.run(*args, **kwargs)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.14 on 2022-11-03 08:44
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('assets', '0108_auto_20221027_1053'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='accountbackupplan',
|
||||||
|
old_name='categories',
|
||||||
|
new_name='types',
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Generated by Django 3.2.14 on 2022-11-03 13:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('assets', '0109_rename_categories_to_types'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='changesecretrecord',
|
||||||
|
name='asset',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.asset'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -111,6 +111,13 @@ class AutomationExecution(OrgModelMixin):
|
||||||
def manager_type(self):
|
def manager_type(self):
|
||||||
return self.snapshot['type']
|
return self.snapshot['type']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def recipients(self):
|
||||||
|
recipients = self.snapshot.get('recipients')
|
||||||
|
if not recipients:
|
||||||
|
return []
|
||||||
|
return recipients.values()
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
from assets.automations.endpoint import ExecutionManager
|
from assets.automations.endpoint import ExecutionManager
|
||||||
manager = ExecutionManager(execution=self)
|
manager = ExecutionManager(execution=self)
|
||||||
|
|
|
@ -51,6 +51,7 @@ class ChangeSecretAutomation(BaseAutomation):
|
||||||
|
|
||||||
class ChangeSecretRecord(JMSBaseModel):
|
class ChangeSecretRecord(JMSBaseModel):
|
||||||
execution = models.ForeignKey('assets.AutomationExecution', on_delete=models.CASCADE)
|
execution = models.ForeignKey('assets.AutomationExecution', on_delete=models.CASCADE)
|
||||||
|
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, null=True)
|
||||||
account = models.ForeignKey('assets.Account', on_delete=models.CASCADE, null=True)
|
account = models.ForeignKey('assets.Account', on_delete=models.CASCADE, null=True)
|
||||||
old_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Old secret'))
|
old_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Old secret'))
|
||||||
new_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
|
new_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
import uuid
|
import uuid
|
||||||
from functools import reduce
|
|
||||||
|
|
||||||
from celery import current_task
|
from celery import current_task
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
@ -11,9 +10,9 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from orgs.mixins.models import OrgModelMixin
|
from orgs.mixins.models import OrgModelMixin
|
||||||
from ops.mixin import PeriodTaskModelMixin
|
from ops.mixin import PeriodTaskModelMixin
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
|
from common.const.choices import Trigger
|
||||||
from common.db.encoder import ModelJSONFieldEncoder
|
from common.db.encoder import ModelJSONFieldEncoder
|
||||||
from common.mixins.models import CommonModelMixin
|
from common.mixins.models import CommonModelMixin
|
||||||
from common.const.choices import Trigger
|
|
||||||
|
|
||||||
__all__ = ['AccountBackupPlan', 'AccountBackupPlanExecution']
|
__all__ = ['AccountBackupPlan', 'AccountBackupPlanExecution']
|
||||||
|
|
||||||
|
@ -22,7 +21,7 @@ logger = get_logger(__file__)
|
||||||
|
|
||||||
class AccountBackupPlan(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin):
|
class AccountBackupPlan(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin):
|
||||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||||
categories = models.JSONField(default=list)
|
types = models.JSONField(default=list)
|
||||||
recipients = models.ManyToManyField(
|
recipients = models.ManyToManyField(
|
||||||
'users.User', related_name='recipient_escape_route_plans', blank=True,
|
'users.User', related_name='recipient_escape_route_plans', blank=True,
|
||||||
verbose_name=_("Recipient")
|
verbose_name=_("Recipient")
|
||||||
|
@ -53,7 +52,7 @@ class AccountBackupPlan(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin):
|
||||||
'crontab': self.crontab,
|
'crontab': self.crontab,
|
||||||
'org_id': self.org_id,
|
'org_id': self.org_id,
|
||||||
'created_by': self.created_by,
|
'created_by': self.created_by,
|
||||||
'categories': self.categories,
|
'types': self.types,
|
||||||
'recipients': {
|
'recipients': {
|
||||||
str(recipient.id): (str(recipient), bool(recipient.secret_key))
|
str(recipient.id): (str(recipient), bool(recipient.secret_key))
|
||||||
for recipient in self.recipients.all()
|
for recipient in self.recipients.all()
|
||||||
|
@ -100,9 +99,9 @@ class AccountBackupPlanExecution(OrgModelMixin):
|
||||||
verbose_name = _('Account backup execution')
|
verbose_name = _('Account backup execution')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def categories(self):
|
def types(self):
|
||||||
categories = self.plan_snapshot.get('categories')
|
types = self.plan_snapshot.get('types')
|
||||||
return categories
|
return types
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def recipients(self):
|
def recipients(self):
|
||||||
|
@ -111,7 +110,11 @@ class AccountBackupPlanExecution(OrgModelMixin):
|
||||||
return []
|
return []
|
||||||
return recipients.values()
|
return recipients.values()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def manager_type(self):
|
||||||
|
return 'backup_account'
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
from ..task_handlers import ExecutionManager
|
from assets.automations.endpoint import ExecutionManager
|
||||||
manager = ExecutionManager(execution=self)
|
manager = ExecutionManager(execution=self)
|
||||||
return manager.run()
|
return manager.run()
|
||||||
|
|
|
@ -2,10 +2,9 @@
|
||||||
#
|
#
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import uuid
|
import sshpubkeys
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
|
|
||||||
import sshpubkeys
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
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 _
|
||||||
|
@ -14,11 +13,11 @@ from django.db.models import QuerySet
|
||||||
|
|
||||||
from common.utils import (
|
from common.utils import (
|
||||||
ssh_key_string_to_obj, ssh_key_gen, get_logger,
|
ssh_key_string_to_obj, ssh_key_gen, get_logger,
|
||||||
random_string, ssh_pubkey_gen,
|
random_string, ssh_pubkey_gen, lazyproperty
|
||||||
)
|
)
|
||||||
from common.db import fields
|
from common.db import fields
|
||||||
from assets.const import Connectivity
|
|
||||||
from orgs.mixins.models import JMSOrgBaseModel
|
from orgs.mixins.models import JMSOrgBaseModel
|
||||||
|
from assets.const import Connectivity, SecretType
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
@ -48,12 +47,6 @@ class AbsConnectivity(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class BaseAccount(JMSOrgBaseModel):
|
class BaseAccount(JMSOrgBaseModel):
|
||||||
class SecretType(models.TextChoices):
|
|
||||||
password = 'password', _('Password')
|
|
||||||
ssh_key = 'ssh_key', _('SSH key')
|
|
||||||
access_key = 'access_key', _('Access key')
|
|
||||||
token = 'token', _('Token')
|
|
||||||
|
|
||||||
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(
|
||||||
|
@ -65,28 +58,34 @@ class BaseAccount(JMSOrgBaseModel):
|
||||||
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
||||||
created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by'))
|
created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by'))
|
||||||
|
|
||||||
@property
|
|
||||||
def password(self):
|
|
||||||
return self.secret
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_secret(self):
|
def has_secret(self):
|
||||||
return bool(self.secret)
|
return bool(self.secret)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def private_key(self):
|
def specific(self):
|
||||||
if self.secret_type == self.SecretType.ssh_key:
|
data = {}
|
||||||
return self.secret
|
if self.secret_type != SecretType.ssh_key:
|
||||||
return None
|
return data
|
||||||
|
data['ssh_key_fingerprint'] = self.ssh_key_fingerprint
|
||||||
|
return data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def public_key(self):
|
def private_key(self):
|
||||||
return ''
|
if self.secret_type == SecretType.ssh_key:
|
||||||
|
return self.secret
|
||||||
|
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 = 'private_key'
|
self.secret_type = SecretType.ssh_key
|
||||||
|
|
||||||
|
@lazyproperty
|
||||||
|
def public_key(self):
|
||||||
|
if self.secret_type == SecretType.ssh_key:
|
||||||
|
return ssh_pubkey_gen(private_key=self.private_key)
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ssh_key_fingerprint(self):
|
def ssh_key_fingerprint(self):
|
||||||
|
@ -94,7 +93,7 @@ class BaseAccount(JMSOrgBaseModel):
|
||||||
public_key = self.public_key
|
public_key = self.public_key
|
||||||
elif self.private_key:
|
elif self.private_key:
|
||||||
try:
|
try:
|
||||||
public_key = ssh_pubkey_gen(private_key=self.private_key, password=self.password)
|
public_key = ssh_pubkey_gen(private_key=self.private_key)
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
return str(e)
|
return str(e)
|
||||||
else:
|
else:
|
||||||
|
@ -107,14 +106,14 @@ class BaseAccount(JMSOrgBaseModel):
|
||||||
@property
|
@property
|
||||||
def private_key_obj(self):
|
def private_key_obj(self):
|
||||||
if self.private_key:
|
if self.private_key:
|
||||||
key_obj = ssh_key_string_to_obj(self.private_key, password=self.password)
|
key_obj = ssh_key_string_to_obj(self.private_key)
|
||||||
return key_obj
|
return key_obj
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def private_key_path(self):
|
def private_key_path(self):
|
||||||
if not self.secret_type != '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')
|
||||||
|
@ -156,7 +155,6 @@ class BaseAccount(JMSOrgBaseModel):
|
||||||
return {
|
return {
|
||||||
'name': self.name,
|
'name': self.name,
|
||||||
'username': self.username,
|
'username': self.username,
|
||||||
'password': self.password,
|
|
||||||
'public_key': self.public_key,
|
'public_key': self.public_key,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,11 +15,35 @@ class AccountBackupExecutionTaskMsg(object):
|
||||||
def message(self):
|
def message(self):
|
||||||
name = self.name
|
name = self.name
|
||||||
if self.user.secret_key:
|
if self.user.secret_key:
|
||||||
return _('{} - The account backup passage task has been completed. See the attachment for details').format(name)
|
return _('{} - The account backup passage task has been completed. See the attachment for details').format(
|
||||||
|
name)
|
||||||
return _("{} - The account backup passage task has been completed: the encryption password has not been set - "
|
return _("{} - The account backup passage task has been completed: the encryption password has not been set - "
|
||||||
"please go to personal information -> file encryption password to set the encryption password").format(name)
|
"please go to personal information -> file encryption password to set the encryption password").format(
|
||||||
|
name)
|
||||||
|
|
||||||
def publish(self, attachment_list=None):
|
def publish(self, attachment_list=None):
|
||||||
send_mail_attachment_async.delay(
|
send_mail_attachment_async(
|
||||||
self.subject, self.message, [self.user.email], attachment_list
|
self.subject, self.message, [self.user.email], attachment_list
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeSecretExecutionTaskMsg(object):
|
||||||
|
subject = _('Notification of implementation result of encryption change plan')
|
||||||
|
|
||||||
|
def __init__(self, name: str, user: User):
|
||||||
|
self.name = name
|
||||||
|
self.user = user
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message(self):
|
||||||
|
name = self.name
|
||||||
|
if self.user.secret_key:
|
||||||
|
return _('{} - The encryption change task has been completed. See the attachment for details').format(name)
|
||||||
|
return _("{} - The encryption change task has been completed: the encryption password has not been set - "
|
||||||
|
"please go to personal information -> file encryption password to set the encryption password").format(
|
||||||
|
name)
|
||||||
|
|
||||||
|
def publish(self, attachments=None):
|
||||||
|
send_mail_attachment_async(
|
||||||
|
self.subject, self.message, [self.user.email], attachments
|
||||||
|
)
|
||||||
|
|
|
@ -11,3 +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 *
|
||||||
|
|
|
@ -3,6 +3,7 @@ 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
|
||||||
|
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
|
||||||
|
|
||||||
|
@ -47,8 +48,7 @@ class AccountSerializerCreateMixin(serializers.ModelSerializer):
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
instance = super().create(validated_data)
|
instance = super().create(validated_data)
|
||||||
if self.push_now:
|
if self.push_now:
|
||||||
# Todo: push it
|
push_accounts_to_assets.delay([instance.id], [instance.asset_id])
|
||||||
print("Start push account to asset")
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -21,9 +21,13 @@ class BaseAccountSerializer(BulkOrgResourceModelSerializer):
|
||||||
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']
|
fields_small = fields_mini + ['privileged', 'secret_type', 'secret', 'has_secret', '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 = [
|
||||||
|
'has_secret', 'specific',
|
||||||
|
'date_verified', 'created_by', 'date_created',
|
||||||
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'secret': {'write_only': True},
|
'secret': {'write_only': True},
|
||||||
'passphrase': {'write_only': True},
|
'passphrase': {'write_only': True},
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from assets.models import AccountTemplate
|
from assets.models import AccountTemplate
|
||||||
from .base import BaseAccountSerializer
|
from .base import BaseAccountSerializer
|
||||||
|
|
||||||
|
@ -9,15 +6,14 @@ class AccountTemplateSerializer(BaseAccountSerializer):
|
||||||
class Meta(BaseAccountSerializer.Meta):
|
class Meta(BaseAccountSerializer.Meta):
|
||||||
model = AccountTemplate
|
model = AccountTemplate
|
||||||
|
|
||||||
@classmethod
|
# @classmethod
|
||||||
def validate_required(cls, attrs):
|
# def validate_required(cls, attrs):
|
||||||
# Todo: why ?
|
# # TODO 选择模版后检查一些必填项
|
||||||
required_field_dict = {}
|
# required_field_dict = {}
|
||||||
error = _('This field is required.')
|
# error = _('This field is required.')
|
||||||
for k, v in cls().fields.items():
|
# for k, v in cls().fields.items():
|
||||||
if v.required and k not in attrs:
|
# if v.required and k not in attrs:
|
||||||
required_field_dict[k] = error
|
# required_field_dict[k] = error
|
||||||
if not required_field_dict:
|
# if not required_field_dict:
|
||||||
return
|
# return
|
||||||
raise serializers.ValidationError(required_field_dict)
|
# raise serializers.ValidationError(required_field_dict)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
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")
|
|
@ -11,8 +11,9 @@ __all__ = [
|
||||||
|
|
||||||
|
|
||||||
@org_aware_func("assets")
|
@org_aware_func("assets")
|
||||||
def push_accounts_to_assets_util(accounts, assets, task_name):
|
def push_accounts_to_assets_util(accounts, assets):
|
||||||
from assets.models import PushAccountAutomation
|
from assets.models import PushAccountAutomation
|
||||||
|
task_name = gettext_noop("Push accounts to assets")
|
||||||
task_name = PushAccountAutomation.generate_unique_name(task_name)
|
task_name = PushAccountAutomation.generate_unique_name(task_name)
|
||||||
account_usernames = list(accounts.values_list('username', flat=True))
|
account_usernames = list(accounts.values_list('username', flat=True))
|
||||||
|
|
||||||
|
@ -33,5 +34,4 @@ def push_accounts_to_assets(account_ids, asset_ids):
|
||||||
assets = Asset.objects.get(id=asset_ids)
|
assets = Asset.objects.get(id=asset_ids)
|
||||||
accounts = Account.objects.get(id=account_ids)
|
accounts = Account.objects.get(id=account_ids)
|
||||||
|
|
||||||
task_name = gettext_noop("Push accounts to assets")
|
return push_accounts_to_assets_util(accounts, assets)
|
||||||
return push_accounts_to_assets_util(accounts, assets, task_name)
|
|
||||||
|
|
|
@ -4,3 +4,4 @@ from .common import *
|
||||||
from .nodes import *
|
from .nodes import *
|
||||||
from .assets import *
|
from .assets import *
|
||||||
from .nodes_with_assets import *
|
from .nodes_with_assets import *
|
||||||
|
from .accounts import *
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
from rest_framework import generics
|
||||||
|
from assets.serializers import AccountSerializer
|
||||||
|
from perms.utils.account import PermAccountUtil
|
||||||
|
from .mixin import RoleAdminMixin, RoleUserMixin
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['UserAllGrantedAccountsApi', 'MyAllGrantedAccountsApi']
|
||||||
|
|
||||||
|
|
||||||
|
class UserAllGrantedAccountsApi(RoleAdminMixin, generics.ListAPIView):
|
||||||
|
""" 授权给用户的所有账号列表 """
|
||||||
|
serializer_class = AccountSerializer
|
||||||
|
filterset_fields = ("name", "username", "privileged", "version")
|
||||||
|
search_fields = filterset_fields
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
util = PermAccountUtil()
|
||||||
|
accounts = util.get_perm_accounts_for_user(self.user)
|
||||||
|
return accounts
|
||||||
|
|
||||||
|
|
||||||
|
class MyAllGrantedAccountsApi(RoleUserMixin, UserAllGrantedAccountsApi):
|
||||||
|
""" 授权给我的所有账号列表 """
|
||||||
|
pass
|
|
@ -58,9 +58,12 @@ user_permission_urlpatterns = [
|
||||||
# 收藏的资产
|
# 收藏的资产
|
||||||
path('<uuid:pk>/nodes/favorite/assets/', api.UserFavoriteGrantedAssetsApi.as_view(), name='user-ungrouped-assets'),
|
path('<uuid:pk>/nodes/favorite/assets/', api.UserFavoriteGrantedAssetsApi.as_view(), name='user-ungrouped-assets'),
|
||||||
path('nodes/favorite/assets/', api.MyFavoriteGrantedAssetsApi.as_view(), name='my-ungrouped-assets'),
|
path('nodes/favorite/assets/', api.MyFavoriteGrantedAssetsApi.as_view(), name='my-ungrouped-assets'),
|
||||||
# v3 中上面的 API 基本不用动
|
|
||||||
|
|
||||||
# 获取所有和资产-用户关联的账号列表
|
# 获取授权给用户的所有账号
|
||||||
|
path('<uuid:pk>/accounts/', api.UserAllGrantedAccountsApi.as_view(), name='user-accounts'),
|
||||||
|
path('accounts/', api.MyAllGrantedAccountsApi.as_view(), name='my-accounts'),
|
||||||
|
|
||||||
|
# 获取授权给用户某个资产的所有账号
|
||||||
path('<uuid:pk>/assets/<uuid:asset_id>/accounts/', api.UserGrantedAssetAccountsApi.as_view(), name='user-asset-accounts'),
|
path('<uuid:pk>/assets/<uuid:asset_id>/accounts/', api.UserGrantedAssetAccountsApi.as_view(), name='user-asset-accounts'),
|
||||||
path('assets/<uuid:asset_id>/accounts/', api.MyGrantedAssetAccountsApi.as_view(), name='my-asset-accounts'),
|
path('assets/<uuid:asset_id>/accounts/', api.MyGrantedAssetAccountsApi.as_view(), name='my-asset-accounts'),
|
||||||
# 用户登录资产的特殊账号, @INPUT, @USER 等
|
# 用户登录资产的特殊账号, @INPUT, @USER 等
|
||||||
|
|
|
@ -27,13 +27,19 @@ class DeployAppletHostManager:
|
||||||
|
|
||||||
def generate_playbook(self):
|
def generate_playbook(self):
|
||||||
playbook_src = os.path.join(CURRENT_DIR, 'playbook.yml')
|
playbook_src = os.path.join(CURRENT_DIR, 'playbook.yml')
|
||||||
|
base_site_url = settings.BASE_SITE_URL
|
||||||
|
bootstrap_token = settings.BOOTSTRAP_TOKEN
|
||||||
|
host_id = str(self.deployment.host.id)
|
||||||
|
if not base_site_url:
|
||||||
|
base_site_url = "localhost:8080"
|
||||||
with open(playbook_src) as f:
|
with open(playbook_src) as f:
|
||||||
plays = yaml.safe_load(f)
|
plays = yaml.safe_load(f)
|
||||||
for play in plays:
|
for play in plays:
|
||||||
play['vars'].update(self.deployment.host.deploy_options)
|
play['vars'].update(self.deployment.host.deploy_options)
|
||||||
play['vars']['DownloadHost'] = settings.BASE_URL + '/download/'
|
play['vars']['DownloadHost'] = base_site_url + '/download/'
|
||||||
play['vars']['CORE_HOST'] = settings.BASE_URL
|
play['vars']['CORE_HOST'] = base_site_url
|
||||||
play['vars']['BOOTSTRAP_TOKEN'] = settings.BOOSTRAP_TOKEN
|
play['vars']['BOOTSTRAP_TOKEN'] = bootstrap_token
|
||||||
|
play['vars']['HOST_ID'] = host_id
|
||||||
play['vars']['HOST_NAME'] = self.deployment.host.name
|
play['vars']['HOST_NAME'] = self.deployment.host.name
|
||||||
|
|
||||||
playbook_dir = os.path.join(self.run_dir, 'playbook')
|
playbook_dir = os.path.join(self.run_dir, 'playbook')
|
||||||
|
@ -70,6 +76,3 @@ class DeployAppletHostManager:
|
||||||
self.deployment.date_finished = timezone.now()
|
self.deployment.date_finished = timezone.now()
|
||||||
with safe_db_connection():
|
with safe_db_connection():
|
||||||
self.deployment.save()
|
self.deployment.save()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
DownloadHost: https://demo.jumpserver.org/download
|
DownloadHost: https://demo.jumpserver.org/download
|
||||||
Initial: 0
|
Initial: 0
|
||||||
HOST_NAME: test
|
HOST_NAME: test
|
||||||
|
HOST_ID: 00000000-0000-0000-0000-000000000000
|
||||||
CORE_HOST: https://demo.jumpserver.org
|
CORE_HOST: https://demo.jumpserver.org
|
||||||
BOOTSTRAP_TOKEN: PleaseChangeMe
|
BOOTSTRAP_TOKEN: PleaseChangeMe
|
||||||
RDS_Licensing: true
|
RDS_Licensing: true
|
||||||
|
@ -13,6 +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
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Install RDS-Licensing (RDS)
|
- name: Install RDS-Licensing (RDS)
|
||||||
|
@ -29,16 +31,26 @@
|
||||||
include_management_tools: yes
|
include_management_tools: yes
|
||||||
register: rds_install
|
register: rds_install
|
||||||
|
|
||||||
- name: Download Jmservisor (jumpserver)
|
- name: Download JumpServer Remoteapp installer (jumpserver)
|
||||||
ansible.windows.win_get_url:
|
ansible.windows.win_get_url:
|
||||||
url: "{{ DownloadHost }}/Jmservisor.msi"
|
url: "{{ DownloadHost }}/{{ TinkerInstaller }}"
|
||||||
dest: "{{ ansible_env.TEMP }}\\Jmservisor.msi"
|
dest: "{{ ansible_env.TEMP }}\\{{ TinkerInstaller }}"
|
||||||
|
|
||||||
- name: Install the Jmservisor (jumpserver)
|
- name: Install JumpServer Remoteapp agent (jumpserver)
|
||||||
ansible.windows.win_package:
|
ansible.windows.win_package:
|
||||||
path: "{{ ansible_env.TEMP }}\\Jmservisor.msi"
|
path: "{{ ansible_env.TEMP }}\\{{ TinkerInstaller }}"
|
||||||
|
arguments:
|
||||||
|
- /VERYSILENT
|
||||||
|
- /SUPPRESSMSGBOXES
|
||||||
|
- /NORESTART
|
||||||
state: present
|
state: present
|
||||||
|
|
||||||
|
- name: Set remote-server on the global system path (remote-server)
|
||||||
|
ansible.windows.win_path:
|
||||||
|
elements:
|
||||||
|
- '%USERPROFILE%\AppData\Local\Programs\JumpServer-Remoteapp\'
|
||||||
|
scope: user
|
||||||
|
|
||||||
- name: Download python-3.10.8
|
- name: Download python-3.10.8
|
||||||
ansible.windows.win_get_url:
|
ansible.windows.win_get_url:
|
||||||
url: "{{ DownloadHost }}/python-3.10.8-amd64.exe"
|
url: "{{ DownloadHost }}/python-3.10.8-amd64.exe"
|
||||||
|
@ -116,12 +128,12 @@
|
||||||
|
|
||||||
- name: Download chromedriver (chrome)
|
- name: Download chromedriver (chrome)
|
||||||
ansible.windows.win_get_url:
|
ansible.windows.win_get_url:
|
||||||
url: "{{ DownloadHost }}/chromedriver_win32.106.zip"
|
url: "{{ DownloadHost }}/chromedriver_win32.107.zip"
|
||||||
dest: "{{ ansible_env.TEMP }}\\chromedriver_win32.106.zip"
|
dest: "{{ ansible_env.TEMP }}\\chromedriver_win32.107.zip"
|
||||||
|
|
||||||
- name: Unzip chromedriver (chrome)
|
- name: Unzip chromedriver (chrome)
|
||||||
community.windows.win_unzip:
|
community.windows.win_unzip:
|
||||||
src: "{{ ansible_env.TEMP }}\\chromedriver_win32.106.zip"
|
src: "{{ ansible_env.TEMP }}\\chromedriver_win32.107.zip"
|
||||||
dest: C:\Program Files\JumpServer\drivers
|
dest: C:\Program Files\JumpServer\drivers
|
||||||
|
|
||||||
- name: Set chromedriver on the global system path (chrome)
|
- name: Set chromedriver on the global system path (chrome)
|
||||||
|
@ -142,8 +154,27 @@
|
||||||
- /quiet
|
- /quiet
|
||||||
|
|
||||||
- name: Generate component config
|
- name: Generate component config
|
||||||
ansible.windows.win_shell: >
|
ansible.windows.win_shell:
|
||||||
echo "Todo: Set config"
|
"remoteapp-server config --hostname {{ HOST_NAME }} --core_host {{ CORE_HOST }}
|
||||||
|
--token {{ BOOTSTRAP_TOKEN }} --host_id {{ HOST_ID }}"
|
||||||
|
|
||||||
|
- name: Install remoteapp-server service
|
||||||
|
ansible.windows.win_shell:
|
||||||
|
"remoteapp-server service install"
|
||||||
|
|
||||||
|
- name: Start remoteapp-server service
|
||||||
|
ansible.windows.win_shell:
|
||||||
|
"remoteapp-server service start"
|
||||||
|
|
||||||
|
- name: Wait Tinker api health
|
||||||
|
ansible.windows.win_uri:
|
||||||
|
url: http://localhost:6068/api/health/
|
||||||
|
status_code: 200
|
||||||
|
method: GET
|
||||||
|
register: _result
|
||||||
|
until: _result.status_code == 200
|
||||||
|
retries: 30
|
||||||
|
delay: 5
|
||||||
|
|
||||||
- name: Sync all remote applets
|
- name: Sync all remote applets
|
||||||
ansible.windows.win_shell: >
|
ansible.windows.win_shell: >
|
||||||
|
|
Loading…
Reference in New Issue