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
|
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')
|
||||||
|
|
|
@ -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
|
# 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',
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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.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
19
jms
|
@ -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()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue