pref: 添加 applet 创建 api

pull/8991/head
ibuler 2022-10-25 12:57:34 +08:00
parent 1372d6322d
commit 5606082ca3
24 changed files with 322 additions and 86 deletions

View File

@ -67,6 +67,7 @@ class HostTypes(BaseType):
return {
cls.LINUX: [
{'name': 'Linux'},
{'name': 'Gateway'}
],
cls.UNIX: [
{'name': 'Unix'},
@ -75,16 +76,31 @@ class HostTypes(BaseType):
{'name': 'AIX', 'automation': {
'push_account_method': 'push_account_aix',
'change_secret_method': 'push_secret_aix'
}},
}}
],
cls.WINDOWS: [
{'name': 'Windows'},
{'name': 'Windows-TLS', 'protocols_setting': {
'rdp': {'security': 'tls'},
}},
{'name': 'Windows-RDP', 'protocols_setting': {
'rdp': {'security': 'rdp'},
}}
{
'name': 'Windows-TLS',
'protocols_setting': {
'rdp': {'security': 'tls'},
}
},
{
'name': 'Windows-RDP',
'protocols_setting': {
'rdp': {'security': 'rdp'},
}
},
{
'name': 'RemoteAppHost',
'_protocols': ['rdp', 'ssh'],
'protocols_setting': {
'ssh': {
'required': True
}
}
}
],
cls.OTHER_HOST: []
}

View File

@ -224,7 +224,10 @@ class AllTypes(ChoicesMixin):
if _protocols:
protocols_data = [p for p in protocols_data if p['name'] in _protocols]
for p in protocols_data:
p['setting'] = {**_protocols_setting.get(p['name'], {}), **p.get('setting', {})}
setting = _protocols_setting.get(p['name'], {})
p['required'] = setting.pop('required', False)
p['default'] = setting.pop('default', False)
p['setting'] = {**setting, **p.get('setting', {})}
platform_data = {
**default_platform_data, **d,

View File

@ -55,7 +55,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='changesecretautomation',
name='secret_strategy',
field=models.CharField(choices=[('specific', 'Specific'), ('random_one', 'All assets use the same random password'), ('random_all', 'All assets use different random password')], default='random_one', max_length=16, verbose_name='Secret strategy'),
field=models.CharField(choices=[('specific', 'Specific'), ('random_one', 'All assets use the same random password'), ('random_all', 'All assets use different random password')], default='specific', max_length=16, verbose_name='Secret strategy'),
),
migrations.AddField(
model_name='changesecretautomation',

View File

@ -1,10 +1,5 @@
# -*- coding: utf-8 -*-
#
from .terminal import *
from .session import *
from .command import *
from .task import *
from .storage import *
from .status import *
from .sharing import *
from .endpoint import *
from .component import *
from .applet import *

View File

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

View File

@ -0,0 +1,71 @@
import os.path
import shutil
import zipfile
import yaml
from django.core.files.storage import default_storage
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from terminal import serializers, models
from terminal.serializers import AppletUploadSerializer
class AppletViewSet(viewsets.ModelViewSet):
queryset = models.Applet.objects.all()
serializer_class = serializers.AppletSerializer
rbac_perms = {
'upload': 'terminal.add_applet',
}
@action(detail=False, methods=['post'], serializer_class=AppletUploadSerializer)
def upload(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
file = serializer.validated_data['file']
save_to = 'applets/{}'.format(file.name + '.tmp.zip')
if default_storage.exists(save_to):
default_storage.delete(save_to)
rel_path = default_storage.save(save_to, file)
path = default_storage.path(rel_path)
extract_to = default_storage.path('applets/{}.tmp'.format(file.name))
if os.path.exists(extract_to):
shutil.rmtree(extract_to)
update = request.query_params.get('update')
with zipfile.ZipFile(path) as zp:
if zp.testzip() is not None:
return Response({'msg': 'Invalid Zip file'}, status=400)
zp.extractall(extract_to)
tmp_dir = os.path.join(extract_to, file.name.replace('.zip', ''))
files = ['manifest.yml', 'icon.png', 'i18n.yml']
for name in files:
path = os.path.join(tmp_dir, name)
if not os.path.exists(path):
return Response({'error': 'Missing file: {}'.format(path)}, status=400)
with open(os.path.join(tmp_dir, 'manifest.yml')) as f:
manifest = yaml.safe_load(f)
name = manifest.get('name', '')
instance = models.Applet.objects.filter(name=name).first()
if instance and not update:
return Response({'error': 'Applet already exists: {}'.format(name)}, status=400)
serializer = serializers.AppletSerializer(data=manifest, instance=instance)
serializer.is_valid(raise_exception=True)
save_to = default_storage.path('applets/{}'.format(name))
if os.path.exists(save_to):
shutil.rmtree(save_to)
shutil.move(tmp_dir, save_to)
serializer.save()
return Response(serializer.data, status=201)
class AppletPublicationViewSet(viewsets.ModelViewSet):
queryset = models.AppletPublication.objects.all()
serializer_class = serializers.AppletPublicationSerializer

View File

@ -0,0 +1,16 @@
from rest_framework import viewsets
from terminal import serializers, models
__all__ = ['AppletHostViewSet', 'AppletHostDeploymentViewSet']
class AppletHostViewSet(viewsets.ModelViewSet):
queryset = models.AppletHost.objects.all()
serializer_class = serializers.AppletHostSerializer
class AppletHostDeploymentViewSet(viewsets.ModelViewSet):
queryset = models.AppletHostDeployment.objects.all()
serializer_class = serializers.AppletHostDeploymentSerializer

View File

@ -0,0 +1,4 @@
from .terminal import *
from .storage import *
from .status import *
from .endpoint import *

View File

@ -2,15 +2,15 @@ from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status
from rest_framework.request import Request
from common.drf.api import JMSBulkModelViewSet
from common.permissions import IsValidUserOrConnectionToken
from django.utils.translation import ugettext_lazy as _
from django.shortcuts import get_object_or_404
from assets.models import Asset
from orgs.utils import tmp_to_root_org
from terminal.models import Session
from ..models import Endpoint, EndpointRule
from .. import serializers
from common.permissions import IsValidUserOrConnectionToken
from terminal.models import Session, Endpoint, EndpointRule
from terminal import serializers
__all__ = ['EndpointViewSet', 'EndpointRuleViewSet']

View File

@ -9,9 +9,9 @@ from rest_framework import viewsets, generics
from rest_framework.views import Response
from rest_framework import status
from ..models import Terminal, Status, Session
from .. import serializers
from ..utils import TypedComponentsStatusMetricsUtil
from terminal.models import Terminal, Status, Session
from terminal import serializers
from terminal.utils import TypedComponentsStatusMetricsUtil
logger = logging.getLogger(__file__)

View File

@ -11,8 +11,8 @@ from django_filters import utils
from terminal import const
from common.const.http import GET
from terminal.filters import CommandStorageFilter, CommandFilter, CommandFilterForStorageTree
from ..models import CommandStorage, ReplayStorage
from ..serializers import CommandStorageSerializer, ReplayStorageSerializer
from terminal.models import CommandStorage, ReplayStorage
from terminal.serializers import CommandStorageSerializer, ReplayStorageSerializer
__all__ = [
'CommandStorageViewSet', 'CommandStorageTestConnectiveApi',

View File

@ -14,9 +14,9 @@ from common.exceptions import JMSException
from common.drf.api import JMSBulkModelViewSet
from common.utils import get_object_or_none, get_request_ip
from common.permissions import WithBootstrapToken
from ..models import Terminal
from .. import serializers
from .. import exceptions
from terminal.models import Terminal
from terminal import serializers
from terminal import exceptions
__all__ = [
'TerminalViewSet', 'TerminalConfig',

View File

@ -0,0 +1,4 @@
from .session import *
from .sharing import *
from .command import *
from .task import *

View File

@ -13,11 +13,11 @@ from common.drf.api import JMSBulkModelViewSet
from common.utils import get_logger
from terminal.backends.command.serializers import InsecureCommandAlertSerializer
from terminal.exceptions import StorageInvalid
from ..backends import (
from terminal.backends import (
get_command_storage, get_multi_command_storage,
SessionCommandSerializer,
)
from ..notifications import CommandAlertMessage
from terminal.notifications import CommandAlertMessage
logger = get_logger(__name__)
__all__ = ['CommandViewSet', 'InsecureCommandAlertAPI']

View File

@ -24,15 +24,16 @@ from common.drf.renders import PassthroughRenderer
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.utils import tmp_to_root_org, tmp_to_org
from users.models import User
from .. import utils
from ..utils import find_session_replay_local, download_session_replay
from ..models import Session
from .. import serializers
from terminal.utils import is_session_approver
from terminal.utils import (
find_session_replay_local, download_session_replay,
is_session_approver, get_session_replay_url
)
from terminal.models import Session
from terminal import serializers
__all__ = [
'SessionViewSet', 'SessionReplayViewSet', 'SessionJoinValidateAPI',
'MySessionAPIView',
'SessionViewSet', 'SessionReplayViewSet',
'SessionJoinValidateAPI', 'MySessionAPIView',
]
logger = get_logger(__name__)
@ -93,7 +94,7 @@ class SessionViewSet(OrgBulkModelViewSet):
url_name='replay-download')
def download(self, request, *args, **kwargs):
session = self.get_object()
local_path, url = utils.get_session_replay_url(session)
local_path, url = get_session_replay_url(session)
if local_path is None:
return Response({"error": url}, status=404)
file = self.prepare_offline_file(session, local_path)

View File

@ -5,9 +5,8 @@ from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from common.const.http import PATCH
from common.permissions import IsValidUser
from orgs.mixins.api import OrgModelViewSet
from .. import serializers, models
from terminal import serializers, models
__all__ = ['SessionSharingViewSet', 'SessionJoinRecordsViewSet']

View File

@ -7,10 +7,10 @@ from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from common.utils import get_object_or_none
from ..models import Session, Task
from .. import serializers
from terminal.utils import is_session_approver
from orgs.utils import tmp_to_root_org
from terminal.models import Session, Task
from terminal import serializers
from terminal.utils import is_session_approver
__all__ = ['TaskViewSet', 'KillSessionAPI', 'KillSessionForTicketAPI']
logger = logging.getLogger(__file__)

View File

@ -1,4 +1,4 @@
# Generated by Django 3.2.14 on 2022-10-21 06:33
# Generated by Django 3.2.14 on 2022-10-24 06:52
from django.db import migrations, models
import django.db.models.deletion
@ -8,6 +8,7 @@ import uuid
class Migration(migrations.Migration):
dependencies = [
('assets', '0110_auto_20221021_1506'),
('terminal', '0053_auto_20220830_1244'),
]
@ -22,10 +23,13 @@ class Migration(migrations.Migration):
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
('version', models.CharField(max_length=16, verbose_name='Version')),
('type', models.CharField(choices=[('app', 'App'), ('web', 'Web')], max_length=16, verbose_name='Type')),
('icon', models.ImageField(upload_to='applet/icon', verbose_name='Icon')),
('author', models.CharField(max_length=128, verbose_name='Author')),
('type', models.CharField(choices=[('general', 'General'), ('web', 'Web')], default='general', max_length=16, verbose_name='Type')),
('path', models.FilePathField(verbose_name='Path')),
('vcs_type', models.CharField(max_length=16, null=True, verbose_name='VCS type')),
('vcs_url', models.CharField(max_length=256, null=True, verbose_name='URL')),
('protocols', models.JSONField(default=list, verbose_name='Protocol')),
('tags', models.JSONField(default=list, verbose_name='Tags')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
],
options={
@ -33,14 +37,13 @@ class Migration(migrations.Migration):
},
),
migrations.CreateModel(
name='AppletProvider',
name='AppletHost',
fields=[
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('account_automation', models.BooleanField(default=False, verbose_name='Account automation')),
('date_synced', models.DateTimeField(blank=True, null=True, verbose_name='Date synced')),
@ -50,22 +53,6 @@ class Migration(migrations.Migration):
'abstract': False,
},
),
migrations.CreateModel(
name='ProviderDeployment',
fields=[
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('status', models.CharField(max_length=16, verbose_name='Status')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('provider', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='terminal.appletprovider', verbose_name='Provider')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='AppletPublication',
fields=[
@ -77,20 +64,36 @@ class Migration(migrations.Migration):
('status', models.CharField(max_length=16, verbose_name='Status')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('applet', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='terminal.applet', verbose_name='Applet')),
('provider', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='terminal.appletprovider', verbose_name='Provider')),
('host', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='terminal.applethost', verbose_name='Host')),
],
options={
'unique_together': {('applet', 'provider')},
'unique_together': {('applet', 'host')},
},
),
migrations.CreateModel(
name='AppletHostDeployment',
fields=[
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('status', models.CharField(max_length=16, verbose_name='Status')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='terminal.applethost', verbose_name='Hosting')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='appletprovider',
model_name='applethost',
name='applets',
field=models.ManyToManyField(through='terminal.AppletPublication', to='terminal.Applet', verbose_name='Applet'),
),
migrations.AddField(
model_name='appletprovider',
name='asset',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='assets.asset', verbose_name='Asset'),
model_name='applethost',
name='host',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='assets.host', verbose_name='Host'),
),
]

View File

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

View File

@ -1,3 +1,7 @@
import yaml
import os.path
from rest_framework.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
@ -9,25 +13,69 @@ __all__ = ['Applet', 'AppletPublication']
class Applet(JMSBaseModel):
class Type(models.TextChoices):
app = 'app', _('App')
general = 'general', _('General')
web = 'web', _('Web')
class VCSType(models.TextChoices):
manual = 'manual', _('Manual')
git = 'git', _('Git')
archive = 'archive', _('Remote gzip')
name = models.CharField(max_length=128, verbose_name=_('Name'), unique=True)
version = models.CharField(max_length=16, verbose_name=_('Version'))
type = models.CharField(max_length=16, choices=Type.choices, verbose_name=_('Type'))
icon = models.ImageField(upload_to='applet/icon', verbose_name=_('Icon'))
author = models.CharField(max_length=128, verbose_name=_('Author'))
path = models.FilePathField(verbose_name=_('Path'))
type = models.CharField(max_length=16, verbose_name=_('Type'), default='general', choices=Type.choices)
vcs_type = models.CharField(max_length=16, verbose_name=_('VCS type'), null=True)
vcs_url = models.CharField(max_length=256, verbose_name=_('URL'), null=True)
protocols = models.JSONField(default=list, verbose_name=_('Protocol'))
tags = models.JSONField(default=list, verbose_name=_('Tags'))
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
def __str__(self):
return self.name
@property
def manifest(self):
path = os.path.join(self.path, 'manifest.yml')
if not os.path.exists(path):
return None
with open(path, 'r') as f:
return yaml.safe_load(f)
@property
def icon(self):
path = os.path.join(self.path, 'icon.png')
if not os.path.exists(path):
return None
with open(path, 'rb') as f:
return f.read()
@classmethod
def validate_manifest(cls, manifest):
fields = ['name', 'display_name', 'version', 'author', 'type', 'tags', 'protocols']
for field in fields:
if field not in manifest:
raise ValidationError(f'Missing field {field}')
if manifest['type'] not in [i[0] for i in cls.Type.choices]:
raise ValidationError('Invalid type')
if not isinstance(manifest['protocols'], list):
raise ValidationError('Invalid protocols')
@classmethod
def create_by_manifest(cls, manifest):
obj = cls()
for k, v in manifest.items():
setattr(obj, k, v)
obj.save()
return obj
class AppletPublication(JMSBaseModel):
applet = models.ForeignKey('Applet', on_delete=models.PROTECT, verbose_name=_('Applet'))
provider = models.ForeignKey('AppletProvider', on_delete=models.PROTECT, verbose_name=_('Provider'))
host = models.ForeignKey('AppletHost', on_delete=models.PROTECT, verbose_name=_('Host'))
status = models.CharField(max_length=16, verbose_name=_('Status'))
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
class Meta:
unique_together = ('applet', 'provider')
unique_together = ('applet', 'host')

View File

@ -1,31 +1,34 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from celery import current_app
from common.db.models import JMSBaseModel
__all__ = ['AppletProvider', 'ProviderDeployment']
__all__ = ['AppletHost', 'AppletHostDeployment']
class AppletProvider(JMSBaseModel):
name = models.CharField(max_length=128, verbose_name=_('Name'), unique=True)
asset = models.ForeignKey('assets.Asset', on_delete=models.PROTECT, verbose_name=_('Asset'))
class AppletHost(JMSBaseModel):
host = models.ForeignKey('assets.Host', on_delete=models.PROTECT, verbose_name=_('Host'))
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
account_automation = models.BooleanField(default=False, verbose_name=_('Account automation'))
date_synced = models.DateTimeField(null=True, blank=True, verbose_name=_('Date synced'))
status = models.CharField(max_length=16, verbose_name=_('Status'))
applets = models.ManyToManyField(
'Applet', verbose_name=_('Applet'),
through='AppletPublication', through_fields=('provider', 'applet'),
through='AppletPublication', through_fields=('host', 'applet'),
)
def __str__(self):
return self.host.name
class ProviderDeployment(JMSBaseModel):
provider = models.ForeignKey('AppletProvider', on_delete=models.CASCADE, verbose_name=_('Provider'))
class AppletHostDeployment(JMSBaseModel):
host = models.ForeignKey('AppletHost', on_delete=models.CASCADE, verbose_name=_('Hosting'))
status = models.CharField(max_length=16, verbose_name=_('Status'))
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
def install(self):
def __str__(self):
return self.host
def start(self):
pass

View File

@ -5,3 +5,4 @@ from .session import *
from .storage import *
from .sharing import *
from .endpoint import *
from .applet import *

View File

@ -0,0 +1,65 @@
from rest_framework import serializers
from common.drf.fields import ObjectRelatedField
from assets.models import Host
from ..models import Applet, AppletPublication, AppletHost, AppletHostDeployment
__all__ = [
'AppletSerializer', 'AppletPublicationSerializer',
'AppletHostSerializer', 'AppletHostDeploymentSerializer',
'AppletUploadSerializer'
]
class AppletSerializer(serializers.ModelSerializer):
class Meta:
model = Applet
fields_mini = ['id', 'name']
read_only_fields = [
'date_created', 'date_updated'
]
fields = fields_mini + [
'version', 'author', 'type', 'protocols', 'comment'
] + read_only_fields
class AppletUploadSerializer(serializers.Serializer):
file = serializers.FileField()
class AppletPublicationSerializer(serializers.ModelSerializer):
applet = ObjectRelatedField(queryset=Applet.objects.all())
host = ObjectRelatedField(queryset=AppletHost.objects.all())
class Meta:
model = AppletPublication
fields_mini = ['id', 'applet', 'host']
read_only_fields = ['date_created', 'date_updated']
fields = fields_mini + [
'status', 'comment',
] + read_only_fields
class AppletHostSerializer(serializers.ModelSerializer):
host = ObjectRelatedField(queryset=Host.objects.all())
class Meta:
model = AppletHost
fields_mini = ['id', 'host']
read_only_fields = ['date_created', 'date_updated']
fields = fields_mini + [
'comment', 'account_automation', 'date_synced', 'status',
] + read_only_fields
class AppletHostDeploymentSerializer(serializers.ModelSerializer):
host = ObjectRelatedField(queryset=AppletHost.objects.all())
class Meta:
model = AppletHostDeployment
fields_mini = ['id', 'host']
read_only_fields = ['date_created', 'date_updated']
fields = fields_mini + [
'status', 'comment',
] + read_only_fields

View File

@ -24,6 +24,11 @@ router.register(r'session-sharings', api.SessionSharingViewSet, 'session-sharing
router.register(r'session-join-records', api.SessionJoinRecordsViewSet, 'session-sharing-record')
router.register(r'endpoints', api.EndpointViewSet, 'endpoint')
router.register(r'endpoint-rules', api.EndpointRuleViewSet, 'endpoint-rule')
router.register(r'applets', api.AppletViewSet, 'applet')
router.register(r'applet-hosts', api.AppletHostViewSet, 'applet-host')
router.register(r'applet-publication', api.AppletPublicationViewSet, 'applet-publication')
router.register(r'applet-host-deployment', api.AppletHostDeploymentViewSet, 'applet-host-deployment')
urlpatterns = [
path('my-sessions/', api.MySessionAPIView.as_view(), name='my-session'),