mirror of https://github.com/jumpserver/jumpserver
perf: 内置 applets 自动安装
parent
5d31200368
commit
754f8131b4
|
@ -35,6 +35,7 @@ def parse_sentinels_host(sentinels_host):
|
|||
VERSION = const.VERSION
|
||||
BASE_DIR = const.BASE_DIR
|
||||
PROJECT_DIR = const.PROJECT_DIR
|
||||
APP_DIR = os.path.join(PROJECT_DIR, 'apps')
|
||||
DATA_DIR = os.path.join(PROJECT_DIR, 'data')
|
||||
ANSIBLE_DIR = os.path.join(DATA_DIR, 'ansible')
|
||||
CERTS_DIR = os.path.join(DATA_DIR, 'certs')
|
||||
|
|
|
@ -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',),
|
||||
),
|
||||
]
|
|
@ -1,16 +1,17 @@
|
|||
# Generated by Django 3.1.13 on 2021-11-19 08:29
|
||||
|
||||
import common.db.models
|
||||
from django.conf import settings
|
||||
import uuid
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.contenttypes.models
|
||||
from django.db import migrations, models
|
||||
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):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
|
@ -28,7 +29,8 @@ class Migration(migrations.Migration):
|
|||
],
|
||||
options={
|
||||
'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': [],
|
||||
},
|
||||
),
|
||||
|
@ -41,8 +43,9 @@ class Migration(migrations.Migration):
|
|||
('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, verbose_name='Name')),
|
||||
('scope', models.CharField(choices=[('system', 'System'), ('org', 'Organization')], default='system', max_length=128, verbose_name='Scope')),
|
||||
('builtin', models.BooleanField(default=False, verbose_name='Built-in')),
|
||||
('scope', models.CharField(choices=[('system', 'System'), ('org', 'Organization')], default='system',
|
||||
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')),
|
||||
],
|
||||
),
|
||||
|
@ -82,10 +85,15 @@ class Migration(migrations.Migration):
|
|||
('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)),
|
||||
('scope', models.CharField(choices=[('system', 'System'), ('org', 'Organization')], default='system', max_length=128, verbose_name='Scope')),
|
||||
('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, 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')),
|
||||
('scope', models.CharField(choices=[('system', 'System'), ('org', 'Organization')], default='system',
|
||||
max_length=128, verbose_name='Scope')),
|
||||
('org', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
|
||||
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={
|
||||
'verbose_name': 'Role binding',
|
||||
|
@ -95,7 +103,8 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='role',
|
||||
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(
|
||||
name='role',
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
from django.utils.translation import ugettext_lazy as _, gettext
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _, gettext
|
||||
|
||||
from common.db.models import JMSBaseModel
|
||||
from common.utils import lazyproperty
|
||||
from .permission import Permission
|
||||
from ..builtin import BuiltinRole
|
||||
from .. import const
|
||||
from ..builtin import BuiltinRole
|
||||
|
||||
__all__ = ['Role', 'SystemRole', 'OrgRole']
|
||||
|
||||
|
@ -33,7 +33,7 @@ class Role(JMSBaseModel):
|
|||
permissions = models.ManyToManyField(
|
||||
'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'))
|
||||
|
||||
BuiltinRole = BuiltinRole
|
||||
|
@ -71,14 +71,14 @@ class Role(JMSBaseModel):
|
|||
@classmethod
|
||||
def get_roles_permissions(cls, roles):
|
||||
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)
|
||||
|
||||
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)
|
||||
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')
|
||||
return permissions
|
||||
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
import os.path
|
||||
import shutil
|
||||
import zipfile
|
||||
import yaml
|
||||
import os.path
|
||||
from typing import Callable
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import default_storage
|
||||
from django.http import HttpResponse
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from common.utils import is_uuid
|
||||
from common.drf.serializers import FileSerializer
|
||||
from common.utils import is_uuid
|
||||
from terminal import serializers
|
||||
from terminal.models import AppletPublication, Applet
|
||||
|
||||
|
||||
__all__ = ['AppletViewSet', 'AppletPublicationViewSet']
|
||||
|
||||
|
||||
|
@ -46,17 +45,7 @@ class DownloadUploadMixin:
|
|||
zp.extractall(extract_to)
|
||||
|
||||
tmp_dir = os.path.join(extract_to, file.name.replace('.zip', ''))
|
||||
files = ['manifest.yml', 'icon.png', 'i18n.yml', 'setup.yml']
|
||||
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'})
|
||||
manifest = Applet.validate_pkg(tmp_dir)
|
||||
return manifest, tmp_dir
|
||||
|
||||
@action(detail=False, methods=['post'], serializer_class=FileSerializer)
|
||||
|
@ -81,7 +70,10 @@ class DownloadUploadMixin:
|
|||
@action(detail=True, methods=['get'])
|
||||
def download(self, request, *args, **kwargs):
|
||||
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)
|
||||
with open(zip_path, 'rb') as f:
|
||||
response = HttpResponse(f.read(), status=200, content_type='application/octet-stream')
|
||||
|
|
|
@ -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()
|
|
@ -0,0 +1,7 @@
|
|||
|
||||
## selenium 版本
|
||||
|
||||
- Selenium == 4.4.0
|
||||
- Chrome 和 ChromeDriver 版本要匹配
|
||||
- Driver [下载地址](https://chromedriver.chromium.org/downloads)
|
||||
|
|
@ -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)
|
|
@ -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')
|
|
@ -0,0 +1,4 @@
|
|||
- zh:
|
||||
display_name: Chrome 浏览器
|
||||
comment: 浏览器打开 URL 页面地址
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
|
@ -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)
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
type: manual # exe, zip, manual
|
||||
source:
|
||||
arguments:
|
||||
destination:
|
||||
program:
|
||||
md5:
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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()
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -7,6 +7,7 @@ from django.core.cache import cache
|
|||
from django.core.files.storage import default_storage
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from common.db.models import JMSBaseModel
|
||||
|
||||
|
@ -24,6 +25,7 @@ class Applet(JMSBaseModel):
|
|||
author = models.CharField(max_length=128, verbose_name=_('Author'))
|
||||
type = models.CharField(max_length=16, verbose_name=_('Type'), default='general', choices=Type.choices)
|
||||
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'))
|
||||
tags = models.JSONField(default=list, verbose_name=_('Tags'))
|
||||
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
|
||||
|
@ -37,7 +39,10 @@ class Applet(JMSBaseModel):
|
|||
|
||||
@property
|
||||
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
|
||||
def manifest(self):
|
||||
|
@ -54,6 +59,33 @@ class Applet(JMSBaseModel):
|
|||
return None
|
||||
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):
|
||||
hosts = list(self.hosts.all())
|
||||
if not hosts:
|
||||
|
@ -73,6 +105,7 @@ class Applet(JMSBaseModel):
|
|||
ttl = 60 * 60 * 24
|
||||
lock_key = 'applet_host_accounts_{}_{}'.format(host.id, account.username)
|
||||
cache.set(lock_key, account.username, ttl)
|
||||
|
||||
return {
|
||||
'host': host,
|
||||
'account': account,
|
||||
|
|
19
jms
19
jms
|
@ -1,14 +1,14 @@
|
|||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
|
||||
import os
|
||||
import argparse
|
||||
import logging
|
||||
import logging.handlers
|
||||
import time
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import django
|
||||
import requests
|
||||
from django.core import management
|
||||
from django.db.utils import OperationalError
|
||||
|
||||
|
@ -24,6 +24,7 @@ logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(message)s", datef
|
|||
|
||||
try:
|
||||
from jumpserver import const
|
||||
|
||||
__version__ = const.VERSION
|
||||
except ImportError as e:
|
||||
print("Not found __version__: {}".format(e))
|
||||
|
@ -122,6 +123,14 @@ def download_ip_db():
|
|||
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():
|
||||
collect_static()
|
||||
perform_db_migrate()
|
||||
|
@ -132,6 +141,7 @@ def prepare():
|
|||
upgrade_db()
|
||||
expire_caches()
|
||||
download_ip_db()
|
||||
install_builtin_applets()
|
||||
|
||||
|
||||
def start_services():
|
||||
|
@ -190,4 +200,3 @@ if __name__ == '__main__':
|
|||
collect_static()
|
||||
else:
|
||||
start_services()
|
||||
|
||||
|
|
Loading…
Reference in New Issue