mirror of https://github.com/jumpserver/jumpserver
commit
0763404235
56
README.md
56
README.md
|
@ -25,7 +25,8 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
|||
- 无插件: 仅需浏览器,极致的 Web Terminal 使用体验;
|
||||
- 多云支持: 一套系统,同时管理不同云上面的资产;
|
||||
- 云端存储: 审计录像云端存储,永不丢失;
|
||||
- 多租户: 一套系统,多个子公司和部门同时使用。
|
||||
- 多租户: 一套系统,多个子公司和部门同时使用;
|
||||
- 多应用支持: 数据库,Windows远程应用,Kubernetes。
|
||||
|
||||
## 版本说明
|
||||
|
||||
|
@ -198,6 +199,54 @@ v2.1.0 是 v2.0.0 之后的功能版本。
|
|||
<td>文件传输</td>
|
||||
<td>可对文件的上传、下载记录进行审计</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="20">数据库审计<br>Database</td>
|
||||
<td rowspan="2">连接方式</td>
|
||||
<td>命令方式</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Web UI方式 (X-PACK)</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td rowspan="4">支持的数据库</td>
|
||||
<td>MySQL</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Oracle (X-PACK)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MariaDB (X-PACK)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PostgreSQL (X-PACK)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="6">功能亮点</td>
|
||||
<td>语法高亮</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SQL格式化</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>支持快捷键</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>支持选中执行</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SQL历史查询</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>支持页面创建 DB, TABLE</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">会话审计</td>
|
||||
<td>命令记录</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>录像回放</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 快速开始
|
||||
|
@ -212,6 +261,11 @@ v2.1.0 是 v2.0.0 之后的功能版本。
|
|||
- [Koko](https://github.com/jumpserver/koko) JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco)
|
||||
- [Guacamole](https://github.com/jumpserver/docker-guacamole) JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/)
|
||||
|
||||
## 致谢
|
||||
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC协议设备,JumpServer 图形化连接依赖
|
||||
- [OmniDB](https://omnidb.org/) Web页面连接使用数据库,JumpServer Web数据库依赖
|
||||
|
||||
|
||||
## JumpServer 企业版
|
||||
- [申请企业版试用](https://jinshuju.net/f/kyOYpi)
|
||||
> 注:企业版支持离线安装,申请通过后会提供高速下载链接。
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 2.2.13 on 2020-11-16 09:57
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0060_node_full_value'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='node',
|
||||
options={'ordering': ['value'], 'verbose_name': 'Node'},
|
||||
),
|
||||
]
|
|
@ -158,9 +158,11 @@ class AuthMixin:
|
|||
if update_fields:
|
||||
self.save(update_fields=update_fields)
|
||||
|
||||
def has_special_auth(self, asset=None):
|
||||
def has_special_auth(self, asset=None, username=None):
|
||||
from .authbook import AuthBook
|
||||
queryset = AuthBook.objects.filter(username=self.username)
|
||||
if username is None:
|
||||
username = self.username
|
||||
queryset = AuthBook.objects.filter(username=username)
|
||||
if asset:
|
||||
queryset = queryset.filter(asset=asset)
|
||||
return queryset.exists()
|
||||
|
|
|
@ -210,6 +210,7 @@ class FamilyMixin:
|
|||
if not full_value:
|
||||
return []
|
||||
nodes_family = full_value.split('/')
|
||||
nodes_family = [v for v in nodes_family if v]
|
||||
org_root = cls.org_root()
|
||||
if nodes_family[0] == org_root.value:
|
||||
nodes_family = nodes_family[1:]
|
||||
|
@ -217,6 +218,7 @@ class FamilyMixin:
|
|||
|
||||
@classmethod
|
||||
def create_nodes_recurse(cls, values, parent=None):
|
||||
values = [v for v in values if v]
|
||||
if not values:
|
||||
return None
|
||||
if parent is None:
|
||||
|
@ -407,7 +409,7 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
|
|||
|
||||
class Meta:
|
||||
verbose_name = _("Node")
|
||||
ordering = ['key']
|
||||
ordering = ['value']
|
||||
|
||||
def __str__(self):
|
||||
return self.full_value
|
||||
|
|
|
@ -165,6 +165,11 @@ class SystemUser(BaseUser):
|
|||
def is_need_test_asset_connective(self):
|
||||
return self.protocol not in self.application_category_protocols
|
||||
|
||||
def has_special_auth(self, asset=None, username=None):
|
||||
if username is None and self.username_same_with_user:
|
||||
raise TypeError('System user is dynamic, username should be pass')
|
||||
return super().has_special_auth(asset=asset, username=username)
|
||||
|
||||
@property
|
||||
def can_perm_to_asset(self):
|
||||
return self.protocol not in self.application_category_protocols
|
||||
|
|
|
@ -139,6 +139,7 @@ def get_push_windows_system_user_tasks(system_user, username=None):
|
|||
|
||||
tasks = []
|
||||
if not password:
|
||||
logger.error("Error: no password found")
|
||||
return tasks
|
||||
task = {
|
||||
'name': 'Add user {}'.format(username),
|
||||
|
@ -214,14 +215,15 @@ def push_system_user_util(system_user, assets, task_name, username=None):
|
|||
print(_("Start push system user for platform: [{}]").format(platform))
|
||||
print(_("Hosts count: {}").format(len(_hosts)))
|
||||
|
||||
if not system_user.has_special_auth():
|
||||
# 如果没有特殊密码设置,就不需要单独推送某台机器了
|
||||
if not system_user.has_special_auth(username=username):
|
||||
logger.debug("System user not has special auth")
|
||||
tasks = get_push_system_user_tasks(system_user, platform, username=username)
|
||||
run_task(tasks, _hosts)
|
||||
continue
|
||||
|
||||
for _host in _hosts:
|
||||
system_user.load_asset_special_auth(_host)
|
||||
system_user.load_asset_special_auth(_host, username=username)
|
||||
tasks = get_push_system_user_tasks(system_user, platform, username=username)
|
||||
run_task(tasks, [_host])
|
||||
|
||||
|
|
|
@ -189,7 +189,7 @@ class Crypto:
|
|||
if origin_text:
|
||||
# 有时不同算法解密不报错,但是返回空字符串
|
||||
return origin_text
|
||||
except (TypeError, ValueError, UnicodeDecodeError):
|
||||
except (TypeError, ValueError, UnicodeDecodeError, IndexError):
|
||||
continue
|
||||
|
||||
|
||||
|
|
|
@ -69,9 +69,9 @@ urlpatterns = [
|
|||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \
|
||||
+ static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
|
||||
js_i18n_patterns = i18n_patterns(
|
||||
path('jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'),
|
||||
)
|
||||
js_i18n_patterns = [
|
||||
path('core/jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'),
|
||||
]
|
||||
urlpatterns += js_i18n_patterns
|
||||
|
||||
handler404 = 'jumpserver.views.handler404'
|
||||
|
|
|
@ -65,6 +65,8 @@ class AdHocResultCallback(CallbackMixin, CallbackModule, CMDCallBackModule):
|
|||
"""
|
||||
Task result Callback
|
||||
"""
|
||||
context = None
|
||||
|
||||
def clean_result(self, t, host, task_name, task_result):
|
||||
contacted = self.results_summary["contacted"]
|
||||
dark = self.results_summary["dark"]
|
||||
|
@ -133,7 +135,11 @@ class AdHocResultCallback(CallbackMixin, CallbackModule, CMDCallBackModule):
|
|||
pass
|
||||
|
||||
def set_play_context(self, context):
|
||||
context.ssh_args = '-C -o ControlMaster=no'
|
||||
# for k, v in context._attributes.items():
|
||||
# print("{} ==> {}".format(k, v))
|
||||
if self.context and isinstance(self.context, dict):
|
||||
for k, v in self.context.items():
|
||||
setattr(context, k, v)
|
||||
|
||||
|
||||
class CommandResultCallback(AdHocResultCallback):
|
||||
|
|
|
@ -182,6 +182,13 @@ class AdHocRunner:
|
|||
_options.update(options)
|
||||
return _options
|
||||
|
||||
def set_control_master_if_need(self, cleaned_tasks):
|
||||
modules = [task.get('action', {}).get('module') for task in cleaned_tasks]
|
||||
if {'ping', 'win_ping'} & set(modules):
|
||||
self.results_callback.context = {
|
||||
'ssh_args': '-C -o ControlMaster=no'
|
||||
}
|
||||
|
||||
def run(self, tasks, pattern, play_name='Ansible Ad-hoc', gather_facts='no'):
|
||||
"""
|
||||
:param tasks: [{'action': {'module': 'shell', 'args': 'ls'}, ...}, ]
|
||||
|
@ -193,6 +200,7 @@ class AdHocRunner:
|
|||
self.check_pattern(pattern)
|
||||
self.results_callback = self.get_result_callback()
|
||||
cleaned_tasks = self.clean_tasks(tasks)
|
||||
self.set_control_master_if_need(cleaned_tasks)
|
||||
context.CLIARGS = ImmutableDict(self.options)
|
||||
|
||||
play_source = dict(
|
||||
|
|
|
@ -76,7 +76,7 @@ class JMSInventory(JMSBaseInventory):
|
|||
write you own inventory, construct you inventory,
|
||||
user_info is obtained from admin_user or asset_user
|
||||
"""
|
||||
def __init__(self, assets, run_as_admin=False, run_as=None, become_info=None):
|
||||
def __init__(self, assets, run_as_admin=False, run_as=None, become_info=None, system_user=None):
|
||||
"""
|
||||
:param assets: assets
|
||||
:param run_as_admin: True 是否使用管理用户去执行, 每台服务器的管理用户可能不同
|
||||
|
@ -86,6 +86,7 @@ class JMSInventory(JMSBaseInventory):
|
|||
self.assets = assets
|
||||
self.using_admin = run_as_admin
|
||||
self.run_as = run_as
|
||||
self.system_user = system_user
|
||||
self.become_info = become_info
|
||||
|
||||
host_list = []
|
||||
|
@ -104,18 +105,25 @@ class JMSInventory(JMSBaseInventory):
|
|||
def get_run_user_info(self, host):
|
||||
from assets.backends import AssetUserManager
|
||||
|
||||
if self.run_as is None:
|
||||
if not self.run_as and not self.system_user:
|
||||
return {}
|
||||
|
||||
asset_id = host.get('id', '')
|
||||
asset = self.assets.filter(id=asset_id).first()
|
||||
if not asset:
|
||||
logger.error('Host not found: ', asset_id)
|
||||
|
||||
if self.system_user:
|
||||
self.system_user.load_asset_special_auth(asset=asset, username=self.run_as)
|
||||
return self.system_user._to_secret_json()
|
||||
|
||||
try:
|
||||
asset = self.assets.get(id=host.get('id'))
|
||||
manager = AssetUserManager()
|
||||
run_user = manager.get_latest(username=self.run_as, asset=asset)
|
||||
run_user = manager.get_latest(username=self.run_as, asset=asset, prefer='system_user')
|
||||
return run_user._to_secret_json()
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
return {}
|
||||
else:
|
||||
return run_user._to_secret_json()
|
||||
|
||||
|
||||
class JMSCustomInventory(JMSBaseInventory):
|
||||
|
|
|
@ -184,7 +184,7 @@ class AdHoc(OrgModelMixin):
|
|||
hid = str(uuid.uuid4())
|
||||
execution = AdHocExecution(
|
||||
id=hid, adhoc=self, task=self.task,
|
||||
task_display=str(self.task),
|
||||
task_display=str(self.task)[:128],
|
||||
date_start=timezone.now(),
|
||||
hosts_amount=self.hosts.count(),
|
||||
)
|
||||
|
|
|
@ -37,7 +37,7 @@ class CommandExecution(OrgModelMixin):
|
|||
username = self.user.username
|
||||
else:
|
||||
username = self.run_as.username
|
||||
inv = JMSInventory(self.hosts.all(), run_as=username)
|
||||
inv = JMSInventory(self.hosts.all(), run_as=username, system_user=self.run_as)
|
||||
return inv
|
||||
|
||||
@lazyproperty
|
||||
|
@ -78,7 +78,7 @@ class CommandExecution(OrgModelMixin):
|
|||
runner = CommandRunner(self.inventory)
|
||||
try:
|
||||
host = self.hosts.first()
|
||||
if host.is_windows():
|
||||
if host and host.is_windows():
|
||||
shell = 'win_shell'
|
||||
else:
|
||||
shell = 'shell'
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
{% include '_head_css_js.html' %}
|
||||
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
|
||||
<script src="{% url 'javascript-catalog' %}"></script>
|
||||
|
||||
<script src="{% static "js/jumpserver.js" %}"></script>
|
||||
<style>
|
||||
.passwordBox {
|
||||
|
|
|
@ -13,5 +13,7 @@
|
|||
<script src="{% static 'js/plugins/sweetalert/sweetalert.min.js' %}"></script>
|
||||
<script src="{% static 'js/bootstrap.min.js' %}"></script>
|
||||
<script src="{% static 'js/plugins/datatables/datatables.min.js' %}"></script>
|
||||
<script src="{% url 'javascript-catalog' %}"></script>
|
||||
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 2.2.13 on 2020-11-16 09:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('terminal', '0028_auto_20201110_1918'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='commandstorage',
|
||||
name='name',
|
||||
field=models.CharField(max_length=128, unique=True, verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='replaystorage',
|
||||
name='name',
|
||||
field=models.CharField(max_length=128, unique=True, verbose_name='Name'),
|
||||
),
|
||||
]
|
|
@ -401,7 +401,7 @@ class CommandStorage(CommonModelMixin):
|
|||
TYPE_DEFAULTS = dict(const.REPLAY_STORAGE_TYPE_CHOICES_DEFAULT).keys()
|
||||
TYPE_SERVER = const.COMMAND_STORAGE_TYPE_SERVER
|
||||
|
||||
name = models.CharField(max_length=32, verbose_name=_("Name"), unique=True)
|
||||
name = models.CharField(max_length=128, verbose_name=_("Name"), unique=True)
|
||||
type = models.CharField(
|
||||
max_length=16, choices=TYPE_CHOICES, verbose_name=_('Type'),
|
||||
default=TYPE_SERVER
|
||||
|
@ -438,7 +438,7 @@ class ReplayStorage(CommonModelMixin):
|
|||
TYPE_SERVER = const.REPLAY_STORAGE_TYPE_SERVER
|
||||
TYPE_DEFAULTS = dict(const.REPLAY_STORAGE_TYPE_CHOICES_DEFAULT).keys()
|
||||
|
||||
name = models.CharField(max_length=32, verbose_name=_("Name"), unique=True)
|
||||
name = models.CharField(max_length=128, verbose_name=_("Name"), unique=True)
|
||||
type = models.CharField(
|
||||
max_length=16, choices=TYPE_CHOICES, verbose_name=_('Type'),
|
||||
default=TYPE_SERVER
|
||||
|
|
Loading…
Reference in New Issue