diff --git a/README.md b/README.md index 5cfc92343..73abee037 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,8 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向 - 无插件: 仅需浏览器,极致的 Web Terminal 使用体验; - 多云支持: 一套系统,同时管理不同云上面的资产; - 云端存储: 审计录像云端存储,永不丢失; -- 多租户: 一套系统,多个子公司和部门同时使用。 +- 多租户: 一套系统,多个子公司和部门同时使用; +- 多应用支持: 数据库,Windows远程应用,Kubernetes。 ## 版本说明 @@ -198,6 +199,54 @@ v2.1.0 是 v2.0.0 之后的功能版本。 文件传输 可对文件的上传、下载记录进行审计 + + 数据库审计
Database + 连接方式 + 命令方式 + + + Web UI方式 (X-PACK) + + + + 支持的数据库 + MySQL + + + Oracle (X-PACK) + + + MariaDB (X-PACK) + + + PostgreSQL (X-PACK) + + + 功能亮点 + 语法高亮 + + + SQL格式化 + + + 支持快捷键 + + + 支持选中执行 + + + SQL历史查询 + + + 支持页面创建 DB, 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) > 注:企业版支持离线安装,申请通过后会提供高速下载链接。 diff --git a/apps/assets/migrations/0061_auto_20201116_1757.py b/apps/assets/migrations/0061_auto_20201116_1757.py new file mode 100644 index 000000000..71ddf21cf --- /dev/null +++ b/apps/assets/migrations/0061_auto_20201116_1757.py @@ -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'}, + ), + ] diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index 282c9b928..b7239da75 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -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() diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index edfec4a32..6461ec955 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -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 diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 1dfd79ad2..5a1f47284 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -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 diff --git a/apps/assets/tasks/push_system_user.py b/apps/assets/tasks/push_system_user.py index 0072a862a..0bb9be407 100644 --- a/apps/assets/tasks/push_system_user.py +++ b/apps/assets/tasks/push_system_user.py @@ -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]) diff --git a/apps/common/utils/crypto.py b/apps/common/utils/crypto.py index e70bd0395..6d8d876ef 100644 --- a/apps/common/utils/crypto.py +++ b/apps/common/utils/crypto.py @@ -189,7 +189,7 @@ class Crypto: if origin_text: # 有时不同算法解密不报错,但是返回空字符串 return origin_text - except (TypeError, ValueError, UnicodeDecodeError): + except (TypeError, ValueError, UnicodeDecodeError, IndexError): continue diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index dd2d02693..624212e6f 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -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' diff --git a/apps/ops/ansible/callback.py b/apps/ops/ansible/callback.py index 3c2fe1b90..adba3e81b 100644 --- a/apps/ops/ansible/callback.py +++ b/apps/ops/ansible/callback.py @@ -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): diff --git a/apps/ops/ansible/runner.py b/apps/ops/ansible/runner.py index 4486bdf1a..e741d8f8b 100644 --- a/apps/ops/ansible/runner.py +++ b/apps/ops/ansible/runner.py @@ -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( diff --git a/apps/ops/inventory.py b/apps/ops/inventory.py index b0fe5f8ef..f3de03210 100644 --- a/apps/ops/inventory.py +++ b/apps/ops/inventory.py @@ -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): diff --git a/apps/ops/models/adhoc.py b/apps/ops/models/adhoc.py index 36ebd77e0..95a1106b1 100644 --- a/apps/ops/models/adhoc.py +++ b/apps/ops/models/adhoc.py @@ -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(), ) diff --git a/apps/ops/models/command.py b/apps/ops/models/command.py index 408c72915..5bf808dce 100644 --- a/apps/ops/models/command.py +++ b/apps/ops/models/command.py @@ -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' diff --git a/apps/templates/_base_only_content.html b/apps/templates/_base_only_content.html index 2c757d344..9da6c0157 100644 --- a/apps/templates/_base_only_content.html +++ b/apps/templates/_base_only_content.html @@ -11,7 +11,7 @@ {% include '_head_css_js.html' %} - +