perf: 内置 applets 自动安装

pull/9223/head
ibuler 2022-12-20 16:48:18 +08:00
parent 5d31200368
commit 754f8131b4
21 changed files with 649 additions and 42 deletions

View File

@ -35,6 +35,7 @@ def parse_sentinels_host(sentinels_host):
VERSION = const.VERSION VERSION = const.VERSION
BASE_DIR = const.BASE_DIR BASE_DIR = const.BASE_DIR
PROJECT_DIR = const.PROJECT_DIR PROJECT_DIR = const.PROJECT_DIR
APP_DIR = os.path.join(PROJECT_DIR, 'apps')
DATA_DIR = os.path.join(PROJECT_DIR, 'data') DATA_DIR = os.path.join(PROJECT_DIR, 'data')
ANSIBLE_DIR = os.path.join(DATA_DIR, 'ansible') ANSIBLE_DIR = os.path.join(DATA_DIR, 'ansible')
CERTS_DIR = os.path.join(DATA_DIR, 'certs') CERTS_DIR = os.path.join(DATA_DIR, 'certs')

View File

@ -0,0 +1,24 @@
# Generated by Django 3.2.14 on 2022-12-20 07:24
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('ops', '0029_auto_20221215_1712'),
]
operations = [
migrations.CreateModel(
name='JobAuditLog',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('ops.jobexecution',),
),
]

View File

@ -1,16 +1,17 @@
# Generated by Django 3.1.13 on 2021-11-19 08:29 # Generated by Django 3.1.13 on 2021-11-19 08:29
import common.db.models import uuid
from django.conf import settings
import django.contrib.auth.models import django.contrib.auth.models
import django.contrib.contenttypes.models import django.contrib.contenttypes.models
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import uuid from django.conf import settings
from django.db import migrations, models
import common.db.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
@ -28,7 +29,8 @@ class Migration(migrations.Migration):
], ],
options={ options={
'verbose_name': 'Menu permission', 'verbose_name': 'Menu permission',
'permissions': [('view_console', 'Can view console view'), ('view_audit', 'Can view audit view'), ('view_workspace', 'Can view workbench view')], 'permissions': [('view_console', 'Can view console view'), ('view_audit', 'Can view audit view'),
('view_workspace', 'Can view workbench view')],
'default_permissions': [], 'default_permissions': [],
}, },
), ),
@ -41,8 +43,9 @@ class Migration(migrations.Migration):
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, verbose_name='Name')), ('name', models.CharField(max_length=128, verbose_name='Name')),
('scope', models.CharField(choices=[('system', 'System'), ('org', 'Organization')], default='system', max_length=128, verbose_name='Scope')), ('scope', models.CharField(choices=[('system', 'System'), ('org', 'Organization')], default='system',
('builtin', models.BooleanField(default=False, verbose_name='Built-in')), max_length=128, verbose_name='Scope')),
('builtin', models.BooleanField(default=False, verbose_name='Builtin')),
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')), ('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
], ],
), ),
@ -82,10 +85,15 @@ class Migration(migrations.Migration):
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), ('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')), ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('scope', models.CharField(choices=[('system', 'System'), ('org', 'Organization')], default='system', max_length=128, verbose_name='Scope')), ('scope', models.CharField(choices=[('system', 'System'), ('org', 'Organization')], default='system',
('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='role_bindings', to='orgs.organization', verbose_name='Organization')), max_length=128, verbose_name='Scope')),
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='role_bindings', to='rbac.role', verbose_name='Role')), ('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
('user', models.ForeignKey(on_delete=common.db.models.CASCADE_SIGNAL_SKIP, related_name='role_bindings', to=settings.AUTH_USER_MODEL, verbose_name='User')), related_name='role_bindings', to='orgs.organization',
verbose_name='Organization')),
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='role_bindings',
to='rbac.role', verbose_name='Role')),
('user', models.ForeignKey(on_delete=common.db.models.CASCADE_SIGNAL_SKIP, related_name='role_bindings',
to=settings.AUTH_USER_MODEL, verbose_name='User')),
], ],
options={ options={
'verbose_name': 'Role binding', 'verbose_name': 'Role binding',
@ -95,7 +103,8 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='role', model_name='role',
name='permissions', name='permissions',
field=models.ManyToManyField(blank=True, related_name='roles', to='rbac.Permission', verbose_name='Permissions'), field=models.ManyToManyField(blank=True, related_name='roles', to='rbac.Permission',
verbose_name='Permissions'),
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name='role', name='role',

View File

@ -1,11 +1,11 @@
from django.utils.translation import ugettext_lazy as _, gettext
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _, gettext
from common.db.models import JMSBaseModel from common.db.models import JMSBaseModel
from common.utils import lazyproperty from common.utils import lazyproperty
from .permission import Permission from .permission import Permission
from ..builtin import BuiltinRole
from .. import const from .. import const
from ..builtin import BuiltinRole
__all__ = ['Role', 'SystemRole', 'OrgRole'] __all__ = ['Role', 'SystemRole', 'OrgRole']
@ -33,7 +33,7 @@ class Role(JMSBaseModel):
permissions = models.ManyToManyField( permissions = models.ManyToManyField(
'rbac.Permission', related_name='roles', blank=True, verbose_name=_('Permissions') 'rbac.Permission', related_name='roles', blank=True, verbose_name=_('Permissions')
) )
builtin = models.BooleanField(default=False, verbose_name=_('Built-in')) builtin = models.BooleanField(default=False, verbose_name=_('Builtin'))
comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment')) comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment'))
BuiltinRole = BuiltinRole BuiltinRole = BuiltinRole
@ -71,14 +71,14 @@ class Role(JMSBaseModel):
@classmethod @classmethod
def get_roles_permissions(cls, roles): def get_roles_permissions(cls, roles):
org_roles = [role for role in roles if role.scope == cls.Scope.org] org_roles = [role for role in roles if role.scope == cls.Scope.org]
org_perms_id = cls.get_scope_roles_perms(org_roles, cls.Scope.org)\ org_perms_id = cls.get_scope_roles_perms(org_roles, cls.Scope.org) \
.values_list('id', flat=True) .values_list('id', flat=True)
system_roles = [role for role in roles if role.scope == cls.Scope.system] system_roles = [role for role in roles if role.scope == cls.Scope.system]
system_perms_id = cls.get_scope_roles_perms(system_roles, cls.Scope.system)\ system_perms_id = cls.get_scope_roles_perms(system_roles, cls.Scope.system) \
.values_list('id', flat=True) .values_list('id', flat=True)
perms_id = set(org_perms_id) | set(system_perms_id) perms_id = set(org_perms_id) | set(system_perms_id)
permissions = Permission.objects.filter(id__in=perms_id)\ permissions = Permission.objects.filter(id__in=perms_id) \
.prefetch_related('content_type') .prefetch_related('content_type')
return permissions return permissions

View File

@ -1,23 +1,22 @@
import os.path
import shutil import shutil
import zipfile import zipfile
import yaml
import os.path
from typing import Callable from typing import Callable
from django.http import HttpResponse from django.conf import settings
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.http import HttpResponse
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from common.utils import is_uuid
from common.drf.serializers import FileSerializer from common.drf.serializers import FileSerializer
from common.utils import is_uuid
from terminal import serializers from terminal import serializers
from terminal.models import AppletPublication, Applet from terminal.models import AppletPublication, Applet
__all__ = ['AppletViewSet', 'AppletPublicationViewSet'] __all__ = ['AppletViewSet', 'AppletPublicationViewSet']
@ -46,17 +45,7 @@ class DownloadUploadMixin:
zp.extractall(extract_to) zp.extractall(extract_to)
tmp_dir = os.path.join(extract_to, file.name.replace('.zip', '')) tmp_dir = os.path.join(extract_to, file.name.replace('.zip', ''))
files = ['manifest.yml', 'icon.png', 'i18n.yml', 'setup.yml'] manifest = Applet.validate_pkg(tmp_dir)
for name in files:
path = os.path.join(tmp_dir, name)
if not os.path.exists(path):
raise ValidationError({'error': 'Missing file {}'.format(name)})
with open(os.path.join(tmp_dir, 'manifest.yml')) as f:
manifest = yaml.safe_load(f)
if not manifest.get('name', ''):
raise ValidationError({'error': 'Missing name in manifest.yml'})
return manifest, tmp_dir return manifest, tmp_dir
@action(detail=False, methods=['post'], serializer_class=FileSerializer) @action(detail=False, methods=['post'], serializer_class=FileSerializer)
@ -81,7 +70,10 @@ class DownloadUploadMixin:
@action(detail=True, methods=['get']) @action(detail=True, methods=['get'])
def download(self, request, *args, **kwargs): def download(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
path = default_storage.path('applets/{}'.format(instance.name)) if instance.builtin:
path = os.path.join(settings.APPS_DIR, 'terminal', 'applets', instance.name)
else:
path = default_storage.path('applets/{}'.format(instance.name))
zip_path = shutil.make_archive(path, 'zip', path) zip_path = shutil.make_archive(path, 'zip', path)
with open(zip_path, 'rb') as f: with open(zip_path, 'rb') as f:
response = HttpResponse(f.read(), status=200, content_type='application/octet-stream') response = HttpResponse(f.read(), status=200, content_type='application/octet-stream')

View File

@ -0,0 +1,22 @@
import os
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
def install_or_update_builtin_applets():
from terminal.models import Applet
applets = os.listdir(BASE_DIR)
for d in applets:
path = os.path.join(BASE_DIR, d)
if not os.path.isdir(path) or not os.path.exists(os.path.join(path, 'manifest.yml')):
continue
print("Install or update applet: {}".format(path))
try:
Applet.install_from_dir(path)
except Exception as e:
print(e)
if __name__ == '__main__':
install_or_update_builtin_applets()

View File

@ -0,0 +1,7 @@
## selenium 版本
- Selenium == 4.4.0
- Chrome 和 ChromeDriver 版本要匹配
- Driver [下载地址](https://chromedriver.chromium.org/downloads)

View File

@ -0,0 +1,201 @@
import time
from enum import Enum
from subprocess import CREATE_NO_WINDOW
from selenium import webdriver
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from common import (Asset, User, Account, Platform)
from common import (notify_err_message, block_input, unblock_input)
from common import (BaseApplication)
class Command(Enum):
TYPE = 'type'
CLICK = 'click'
OPEN = 'open'
def _execute_type(ele: WebElement, value: str):
ele.send_keys(value)
def _execute_click(ele: WebElement, value: str):
ele.click()
commands_func_maps = {
Command.CLICK: _execute_click,
Command.TYPE: _execute_type,
Command.OPEN: _execute_type,
}
class StepAction:
methods_map = {
"NAME": By.NAME,
"ID": By.ID,
"CLASS_NAME": By.CLASS_NAME,
"CSS_SELECTOR": By.CSS_SELECTOR,
"CSS": By.CSS_SELECTOR,
"XPATH": By.XPATH
}
def __init__(self, target='', value='', command=Command.TYPE, **kwargs):
self.target = target
self.value = value
self.command = command
def execute(self, driver: webdriver.Chrome) -> bool:
if not self.target:
return True
target_name, target_value = self.target.split("=", 1)
by_name = self.methods_map.get(target_name.upper(), By.NAME)
ele = driver.find_element(by=by_name, value=target_value)
if not ele:
return False
if self.command == 'type':
ele.send_keys(self.value)
elif self.command in ['click', 'button']:
ele.click()
elif self.command in ['open']:
driver.get(self.value)
return True
def _execute_command_type(self, ele, value):
ele.send_keys(value)
def execute_action(driver: webdriver.Chrome, step: StepAction) -> bool:
try:
return step.execute(driver)
except Exception as e:
print(e)
notify_err_message(str(e))
return False
class WebAPP(object):
def __init__(self, app_name: str = '', user: User = None, asset: Asset = None,
account: Account = None, platform: Platform = None, **kwargs):
self.app_name = app_name
self.user = user
self.asset = asset
self.account = account
self.platform = platform
self.extra_data = self.asset.specific
self._steps = list()
autofill_type = self.asset.specific.autofill
if autofill_type == "basic":
self._steps = self._default_custom_steps()
elif autofill_type == "script":
steps = sorted(self.asset.specific.script, key=lambda step_item: step_item['step'])
for item in steps:
val = item['value']
if val:
val = val.replace("{USERNAME}", self.account.username)
val = val.replace("{SECRET}", self.account.secret)
item['value'] = val
self._steps.append(item)
def _default_custom_steps(self) -> list:
account = self.account
specific_property = self.asset.specific
default_steps = [
{
"step": 1,
"value": account.username,
"target": specific_property.username_selector,
"command": "type"
},
{
"step": 2,
"value": account.secret,
"target": specific_property.password_selector,
"command": "type"
},
{
"step": 3,
"value": "",
"target": specific_property.submit_selector,
"command": "click"
}
]
return default_steps
def execute(self, driver: webdriver.Chrome) -> bool:
if not self.asset.address:
return True
for step in self._steps:
action = StepAction(**step)
ret = execute_action(driver, action)
if not ret:
unblock_input()
notify_err_message(f"执行失败: target: {action.target} command: {action.command}")
block_input()
return False
return True
def default_chrome_driver_options():
options = webdriver.ChromeOptions()
options.add_argument("start-maximized")
# 禁用 扩展
options.add_argument("--disable-extensions")
# 禁用开发者工具
options.add_argument("--disable-dev-tools")
# 禁用 密码管理器弹窗
prefs = {"credentials_enable_service": False,
"profile.password_manager_enabled": False}
options.add_experimental_option("prefs", prefs)
options.add_experimental_option("excludeSwitches", ['enable-automation'])
return options
class AppletApplication(BaseApplication):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.driver = None
self.app = WebAPP(app_name=self.app_name, user=self.user,
account=self.account, asset=self.asset, platform=self.platform)
self._chrome_options = default_chrome_driver_options()
def run(self):
service = Service()
# driver 的 console 终端框不显示
service.creationflags = CREATE_NO_WINDOW
self.driver = webdriver.Chrome(options=self._chrome_options, service=service)
self.driver.implicitly_wait(10)
if self.app.asset.address != "":
self.driver.get(self.app.asset.address)
ok = self.app.execute(self.driver)
if not ok:
print("执行失败")
self.driver.maximize_window()
def wait(self):
msg = "Unable to evaluate script: disconnected: not connected to DevTools\n"
while True:
time.sleep(5)
logs = self.driver.get_log('driver')
if len(logs) == 0:
continue
ret = logs[-1]
if isinstance(ret, dict):
if ret.get("message") == msg:
print(ret)
break
self.close()
def close(self):
if self.driver:
try:
self.driver.close()
except Exception as e:
print(e)

View File

@ -0,0 +1,197 @@
import abc
import subprocess
import sys
import time
import os
import json
import base64
from subprocess import CREATE_NO_WINDOW
_blockInput = None
_messageBox = None
if sys.platform == 'win32':
import ctypes
from ctypes import wintypes
import win32ui
# import win32con
_messageBox = win32ui.MessageBox
_blockInput = ctypes.windll.user32.BlockInput
_blockInput.argtypes = [wintypes.BOOL]
_blockInput.restype = wintypes.BOOL
def block_input():
if _blockInput:
_blockInput(True)
def unblock_input():
if _blockInput:
_blockInput(False)
def notify_err_message(msg):
if _messageBox:
_messageBox(msg, 'Error')
def check_pid_alive(pid) -> bool:
# tasklist /fi "PID eq 508" /fo csv
# '"映像名称","PID","会话名 ","会话# ","内存使用 "\r\n"wininit.exe","508","Services","0","6,920 K"\r\n'
try:
csv_ret = subprocess.check_output(["tasklist", "/fi", f'PID eq {pid}', "/fo", "csv"],
creationflags=CREATE_NO_WINDOW)
content = csv_ret.decode()
content_list = content.strip().split("\r\n")
if len(content_list) != 2:
notify_err_message(content)
return False
ret_pid = content_list[1].split(",")[1].strip('"')
return str(pid) == ret_pid
except Exception as e:
notify_err_message(e)
return False
def wait_pid(pid):
while 1:
time.sleep(5)
ok = check_pid_alive(pid)
if not ok:
notify_err_message("程序退出")
break
class DictObj:
def __init__(self, in_dict: dict):
assert isinstance(in_dict, dict)
for key, val in in_dict.items():
if isinstance(val, (list, tuple)):
setattr(self, key, [DictObj(x) if isinstance(x, dict) else x for x in val])
else:
setattr(self, key, DictObj(val) if isinstance(val, dict) else val)
class User(DictObj):
id: str
name: str
username: str
class Specific(DictObj):
# web
autofill: str
username_selector: str
password_selector: str
submit_selector: str
script: list
# database
db_name: str
class Category(DictObj):
value: str
label: str
class Protocol(DictObj):
id: str
name: str
port: int
class Asset(DictObj):
id: str
name: str
address: str
protocols: list[Protocol]
category: Category
specific: Specific
def get_protocol_port(self, protocol):
for item in self.protocols:
if item.name == protocol:
return item.port
return None
class LabelValue(DictObj):
label: str
value: str
class Account(DictObj):
id: str
name: str
username: str
secret: str
secret_type: LabelValue
class Platform(DictObj):
id: str
name: str
charset: LabelValue
type: LabelValue
class Manifest(DictObj):
name: str
version: str
path: str
exec_type: str
connect_type: str
protocols: list[str]
def get_manifest_data() -> dict:
current_dir = os.path.dirname(__file__)
manifest_file = os.path.join(current_dir, 'manifest.json')
try:
with open(manifest_file, "r", encoding='utf8') as f:
return json.load(f)
except Exception as e:
print(e)
return {}
def read_app_manifest(app_dir) -> dict:
main_json_file = os.path.join(app_dir, "manifest.json")
if not os.path.exists(main_json_file):
return {}
with open(main_json_file, 'r', encoding='utf8') as f:
return json.load(f)
def convert_base64_to_dict(base64_str: str) -> dict:
try:
data_json = base64.decodebytes(base64_str.encode('utf-8')).decode('utf-8')
return json.loads(data_json)
except Exception as e:
print(e)
return {}
class BaseApplication(abc.ABC):
def __init__(self, *args, **kwargs):
self.app_name = kwargs.get('app_name', '')
self.protocol = kwargs.get('protocol', '')
self.manifest = Manifest(kwargs.get('manifest', {}))
self.user = User(kwargs.get('user', {}))
self.asset = Asset(kwargs.get('asset', {}))
self.account = Account(kwargs.get('account', {}))
self.platform = Platform(kwargs.get('platform', {}))
@abc.abstractmethod
def run(self):
raise NotImplementedError('run')
@abc.abstractmethod
def wait(self):
raise NotImplementedError('wait')

View File

@ -0,0 +1,4 @@
- zh:
display_name: Chrome 浏览器
comment: 浏览器打开 URL 页面地址

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,22 @@
import sys
from common import (block_input, unblock_input)
from common import convert_base64_to_dict
from app import AppletApplication
def main():
base64_str = sys.argv[1]
data = convert_base64_to_dict(base64_str)
applet_app = AppletApplication(**data)
block_input()
applet_app.run()
unblock_input()
applet_app.wait()
if __name__ == '__main__':
try:
main()
except Exception as e:
print(e)

View File

@ -0,0 +1,12 @@
name: chrome
display_name: Chrome Browser
version: 0.1
comment: Chrome Browser Open URL Page Address
author: JumpServer Team
exec_type: python
update_policy: always
type: web
tags:
- web
protocols:
- http

View File

@ -0,0 +1,6 @@
type: manual # exe, zip, manual
source:
arguments:
destination:
program:
md5:

View File

@ -0,0 +1,41 @@
{
"protocol": "web",
"user": {
"id": "2647CA35-5CAD-4DDF-8A88-6BD88F39BB30",
"name": "Administrator",
"username": "admin"
},
"asset": {
"id": "46EE5F50-F1C1-468C-97EE-560E3436754C",
"name": "test_baidu",
"address": "https://www.baidu.com",
"category": {
"value": "web",
"label": "web"
},
"protocols": [
{
"id": 2,
"name": "http",
"port": 80
}
],
"specific": {
"autofill": "basic",
"username_selector": "name=username",
"password_selector": "name=password",
"submit_selector": "id=longin_button",
"script": []
},
"org_id": "2925D985-A435-411D-9BC4-FEA630F105D9"
},
"account": {
"id": "9D5585DE-5132-458C-AABE-89A83C112A83",
"name": "test_mysql",
"username": "root",
"secret": ""
},
"platform": {
"charset": "UTF-8"
}
}

View File

View File

@ -0,0 +1,9 @@
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = 'Install builtin applets'
def handle(self, *args, **options):
from terminal.applets import install_or_update_builtin_applets
install_or_update_builtin_applets()

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.14 on 2022-12-20 07:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('terminal', '0062_auto_20221216_1529'),
]
operations = [
migrations.AddField(
model_name='applet',
name='builtin',
field=models.BooleanField(default=False, verbose_name='Builtin'),
),
]

View File

@ -7,6 +7,7 @@ from django.core.cache import cache
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import ValidationError
from common.db.models import JMSBaseModel from common.db.models import JMSBaseModel
@ -24,6 +25,7 @@ class Applet(JMSBaseModel):
author = models.CharField(max_length=128, verbose_name=_('Author')) author = models.CharField(max_length=128, verbose_name=_('Author'))
type = models.CharField(max_length=16, verbose_name=_('Type'), default='general', choices=Type.choices) type = models.CharField(max_length=16, verbose_name=_('Type'), default='general', choices=Type.choices)
is_active = models.BooleanField(default=True, verbose_name=_('Is active')) is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
builtin = models.BooleanField(default=False, verbose_name=_('Builtin'))
protocols = models.JSONField(default=list, verbose_name=_('Protocol')) protocols = models.JSONField(default=list, verbose_name=_('Protocol'))
tags = models.JSONField(default=list, verbose_name=_('Tags')) tags = models.JSONField(default=list, verbose_name=_('Tags'))
comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
@ -37,7 +39,10 @@ class Applet(JMSBaseModel):
@property @property
def path(self): def path(self):
return default_storage.path('applets/{}'.format(self.name)) if self.builtin:
return os.path.join(settings.APPS_DIR, 'terminal', 'applets', self.name)
else:
return default_storage.path('applets/{}'.format(self.name))
@property @property
def manifest(self): def manifest(self):
@ -54,6 +59,33 @@ class Applet(JMSBaseModel):
return None return None
return os.path.join(settings.MEDIA_URL, 'applets', self.name, 'icon.png') return os.path.join(settings.MEDIA_URL, 'applets', self.name, 'icon.png')
@staticmethod
def validate_pkg(d):
files = ['manifest.yml', 'icon.png', 'i18n.yml', 'setup.yml']
for name in files:
path = os.path.join(d, name)
if not os.path.exists(path):
raise ValidationError({'error': 'Missing file {}'.format(path)})
with open(os.path.join(d, 'manifest.yml')) as f:
manifest = yaml.safe_load(f)
if not manifest.get('name', ''):
raise ValidationError({'error': 'Missing name in manifest.yml'})
return manifest
@classmethod
def install_from_dir(cls, path):
from terminal.serializers import AppletSerializer
manifest = cls.validate_pkg(path)
name = manifest['name']
instance = cls.objects.filter(name=name).first()
serializer = AppletSerializer(instance=instance, data=manifest)
serializer.is_valid()
serializer.save(builtin=True)
return instance
def select_host_account(self): def select_host_account(self):
hosts = list(self.hosts.all()) hosts = list(self.hosts.all())
if not hosts: if not hosts:
@ -73,6 +105,7 @@ class Applet(JMSBaseModel):
ttl = 60 * 60 * 24 ttl = 60 * 60 * 24
lock_key = 'applet_host_accounts_{}_{}'.format(host.id, account.username) lock_key = 'applet_host_accounts_{}_{}'.format(host.id, account.username)
cache.set(lock_key, account.username, ttl) cache.set(lock_key, account.username, ttl)
return { return {
'host': host, 'host': host,
'account': account, 'account': account,

19
jms
View File

@ -1,14 +1,14 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# coding: utf-8 # coding: utf-8
import os import argparse
import logging import logging
import logging.handlers import logging.handlers
import time import os
import argparse
import sys import sys
import time
import django import django
import requests
from django.core import management from django.core import management
from django.db.utils import OperationalError from django.db.utils import OperationalError
@ -24,6 +24,7 @@ logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(message)s", datef
try: try:
from jumpserver import const from jumpserver import const
__version__ = const.VERSION __version__ = const.VERSION
except ImportError as e: except ImportError as e:
print("Not found __version__: {}".format(e)) print("Not found __version__: {}".format(e))
@ -122,6 +123,14 @@ def download_ip_db():
download_file(src, path) download_file(src, path)
def install_builtin_applets():
logging.info("Install builtin applets")
try:
management.call_command('install_builtin_applets', verbosity=0, interactive=False)
except:
pass
def upgrade_db(): def upgrade_db():
collect_static() collect_static()
perform_db_migrate() perform_db_migrate()
@ -132,6 +141,7 @@ def prepare():
upgrade_db() upgrade_db()
expire_caches() expire_caches()
download_ip_db() download_ip_db()
install_builtin_applets()
def start_services(): def start_services():
@ -190,4 +200,3 @@ if __name__ == '__main__':
collect_static() collect_static()
else: else:
start_services() start_services()