-
+
{% for user_group in user_groups_remain %}
{{ user_group }}
{% endfor %}
@@ -128,7 +111,7 @@
{% for user_group in asset_permission.user_groups.all %}
-
+
{{ user_group }}
@@ -145,120 +128,124 @@
-
{% endblock %}
{% block custom_foot_js %}
{% endblock %}
diff --git a/apps/perms/templates/perms/database_app_permission_create_update.html b/apps/perms/templates/perms/database_app_permission_create_update.html
new file mode 100644
index 000000000..a779e5826
--- /dev/null
+++ b/apps/perms/templates/perms/database_app_permission_create_update.html
@@ -0,0 +1,143 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% load static %}
+{% load bootstrap3 %}
+{% block custom_head_css_js %}
+
+{% endblock %}
+
+{% block content %}
+
+{% endblock %}
+{% block custom_foot_js %}
+
+
+
+
+
+
+{% endblock %}
diff --git a/apps/perms/templates/perms/database_app_permission_database_app.html b/apps/perms/templates/perms/database_app_permission_database_app.html
new file mode 100644
index 000000000..0a23618d0
--- /dev/null
+++ b/apps/perms/templates/perms/database_app_permission_database_app.html
@@ -0,0 +1,237 @@
+{% extends 'base.html' %}
+{% load static %}
+{% load i18n %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+
+
{% trans 'DatabaseApp list of ' %} {{ database_app_permission.name }}
+
+
+
+
+
+
+
+
+ {% trans 'Add DatabaseApp to this permission' %}
+
+
+
+
+
+
+ {% trans 'System user' %}
+
+
+
+
+
+
+ {% for system_user in object.system_users.all %}
+
+ {{ system_user|truncatechars:21}}
+
+
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
+{% block custom_foot_js %}
+
+{% endblock %}
diff --git a/apps/perms/templates/perms/database_app_permission_detail.html b/apps/perms/templates/perms/database_app_permission_detail.html
new file mode 100644
index 000000000..feae4db24
--- /dev/null
+++ b/apps/perms/templates/perms/database_app_permission_detail.html
@@ -0,0 +1,157 @@
+{% extends 'base.html' %}
+{% load static %}
+{% load i18n %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% trans 'Name' %}:
+ {{ object.name }}
+
+
+ {% trans 'User count' %}:
+ {{ object.users.count }}
+
+
+ {% trans 'User group count' %}:
+ {{ object.user_groups.count }}
+
+
+ {% trans 'DatabaseApp count' %}:
+ {{ object.database_apps.count }}
+
+
+ {% trans 'System user count' %}:
+ {{ object.system_users.count }}
+
+
+ {% trans 'Date start' %}:
+ {{ object.date_start }}
+
+
+ {% trans 'Date expired' %}:
+ {{ object.date_expired }}
+
+
+ {% trans 'Date created' %}:
+ {{ object.date_created }}
+
+
+ {% trans 'Created by' %}:
+ {{ object.created_by }}
+
+
+ {% trans 'Comment' %}:
+ {{ object.comment }}
+
+
+
+
+
+
+
+
+
+
+ {% trans 'Quick update' %}
+
+
+
+
+
+ {% trans 'Active' %} :
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
+{% block custom_foot_js %}
+
+{% endblock %}
diff --git a/apps/perms/templates/perms/database_app_permission_list.html b/apps/perms/templates/perms/database_app_permission_list.html
new file mode 100644
index 000000000..b85454cb8
--- /dev/null
+++ b/apps/perms/templates/perms/database_app_permission_list.html
@@ -0,0 +1,99 @@
+{% extends '_base_list.html' %}
+{% load i18n static %}
+{% block table_search %}{% endblock %}
+{% block table_container %}
+
+
+{% endblock %}
+{% block content_bottom_left %}{% endblock %}
+{% block custom_foot_js %}
+
+{% endblock %}
diff --git a/apps/perms/templates/perms/database_app_permission_user.html b/apps/perms/templates/perms/database_app_permission_user.html
new file mode 100644
index 000000000..603f60a6e
--- /dev/null
+++ b/apps/perms/templates/perms/database_app_permission_user.html
@@ -0,0 +1,251 @@
+{% extends 'base.html' %}
+{% load static %}
+{% load i18n %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+
+
{% trans 'User list of ' %} {{ database_app_permission.name }}
+
+
+
+
+
+
+
+
+ {% trans 'Add user to permission' %}
+
+
+
+
+
+
+ {% trans 'Add user group to permission' %}
+
+
+
+
+
+
+ {% for user_group in database_app_permission.user_groups.all %}
+
+ {{ user_group }}
+
+
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
+{% block custom_foot_js %}
+
+{% endblock %}
diff --git a/apps/perms/templates/perms/remote_app_permission_create_update.html b/apps/perms/templates/perms/remote_app_permission_create_update.html
index b7652a59b..f6e0cda7d 100644
--- a/apps/perms/templates/perms/remote_app_permission_create_update.html
+++ b/apps/perms/templates/perms/remote_app_permission_create_update.html
@@ -3,8 +3,6 @@
{% load static %}
{% load bootstrap3 %}
{% block custom_head_css_js %}
-
-
{% endblock %}
@@ -125,7 +123,7 @@ $(document).ready(function () {
var method = "POST";
var the_url = '{% url "api-perms:remote-app-permission-list" %}';
var redirect_to = '{% url "perms:remote-app-permission-list" %}';
- {% if type == "update" %}
+ {% if api_action == "update" %}
the_url = '{% url "api-perms:remote-app-permission-detail" pk=object.id %}';
method = "PUT";
{% endif %}
diff --git a/apps/perms/templates/perms/remote_app_permission_detail.html b/apps/perms/templates/perms/remote_app_permission_detail.html
index f2ec8fa35..aedef832e 100644
--- a/apps/perms/templates/perms/remote_app_permission_detail.html
+++ b/apps/perms/templates/perms/remote_app_permission_detail.html
@@ -2,11 +2,6 @@
{% load static %}
{% load i18n %}
-{% block custom_head_css_js %}
-
-
-{% endblock %}
-
{% block content %}
@@ -244,4 +239,4 @@ $(document).ready(function () {
});
})
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/apps/perms/templates/perms/remote_app_permission_list.html b/apps/perms/templates/perms/remote_app_permission_list.html
index 3812b747b..f6071430a 100644
--- a/apps/perms/templates/perms/remote_app_permission_list.html
+++ b/apps/perms/templates/perms/remote_app_permission_list.html
@@ -75,7 +75,7 @@ function initTable() {
{data: "remote_apps", orderable: false},
{data: "system_users", orderable: false},
{data: "is_valid", orderable: false},
- {data: "id", orderable: false}
+ {data: "id", orderable: false, width: "120px"}
],
op_html: $('#actions').html()
};
diff --git a/apps/perms/templates/perms/remote_app_permission_remote_app.html b/apps/perms/templates/perms/remote_app_permission_remote_app.html
index 091613317..991489033 100644
--- a/apps/perms/templates/perms/remote_app_permission_remote_app.html
+++ b/apps/perms/templates/perms/remote_app_permission_remote_app.html
@@ -2,10 +2,6 @@
{% load static %}
{% load i18n %}
-{% block custom_head_css_js %}
-
-
-{% endblock %}
{% block content %}
@@ -161,4 +157,4 @@
removeRemoteApps(remote_apps)
})
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/apps/perms/templates/perms/remote_app_permission_user.html b/apps/perms/templates/perms/remote_app_permission_user.html
index e18e4f694..d222dfb7c 100644
--- a/apps/perms/templates/perms/remote_app_permission_user.html
+++ b/apps/perms/templates/perms/remote_app_permission_user.html
@@ -2,10 +2,6 @@
{% load static %}
{% load i18n %}
-{% block custom_head_css_js %}
-
-
-{% endblock %}
{% block content %}
@@ -253,4 +249,4 @@
$tr.remove()
})
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/apps/perms/urls/api_urls.py b/apps/perms/urls/api_urls.py
index 5ffa0d14f..9ec3b754f 100644
--- a/apps/perms/urls/api_urls.py
+++ b/apps/perms/urls/api_urls.py
@@ -1,115 +1,20 @@
# coding:utf-8
-from django.urls import path, re_path
-from rest_framework import routers
+from django.urls import re_path
from common import api as capi
-from .. import api
+from .asset_permission import asset_permission_urlpatterns
+from .remote_app_permission import remote_app_permission_urlpatterns
+from .database_app_permission import database_app_permission_urlpatterns
app_name = 'perms'
-router = routers.DefaultRouter()
-router.register('asset-permissions', api.AssetPermissionViewSet, 'asset-permission')
-router.register('remote-app-permissions', api.RemoteAppPermissionViewSet, 'remote-app-permission')
-
-
-asset_permission_urlpatterns = [
- # Assets
- path('users/
/assets/', api.UserGrantedAssetsApi.as_view(), name='user-assets'),
- path('users/assets/', api.UserGrantedAssetsApi.as_view(), name='my-assets'),
-
- # Assets as tree
- path('users//assets/tree/', api.UserGrantedAssetsAsTreeApi.as_view(), name='user-assets-as-tree'),
- path('users/assets/tree/', api.UserGrantedAssetsAsTreeApi.as_view(), name='my-assets-as-tree'),
-
- # Nodes
- path('users//nodes/', api.UserGrantedNodesApi.as_view(), name='user-nodes'),
- path('users/nodes/', api.UserGrantedNodesApi.as_view(), name='my-nodes'),
-
- # Node children
- path('users//nodes/children/', api.UserGrantedNodesApi.as_view(), name='user-nodes-children'),
- path('users/nodes/children/', api.UserGrantedNodesApi.as_view(), name='my-nodes-children'),
-
- # Node as tree
- path('users//nodes/tree/', api.UserGrantedNodesAsTreeApi.as_view(), name='user-nodes-as-tree'),
- path('users/nodes/tree/', api.UserGrantedNodesAsTreeApi.as_view(), name='my-nodes-as-tree'),
-
- # Node with assets as tree
- path('users//nodes-with-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='user-nodes-with-assets-as-tree'),
- path('users/nodes-with-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='my-nodes-with-assets-as-tree'),
-
- # Node children as tree
- path('users//nodes/children/tree/', api.UserGrantedNodeChildrenAsTreeApi.as_view(), name='user-nodes-children-as-tree'),
- path('users/nodes/children/tree/', api.UserGrantedNodeChildrenAsTreeApi.as_view(), name='my-nodes-children-as-tree'),
-
- # Node children with assets as tree
- path('users//nodes/children-with-assets/tree/', api.UserGrantedNodeChildrenWithAssetsAsTreeApi.as_view(), name='user-nodes-children-with-assets-as-tree'),
- path('users/nodes/children-with-assets/tree/', api.UserGrantedNodeChildrenWithAssetsAsTreeApi.as_view(), name='my-nodes-children-with-assets-as-tree'),
-
- # Node assets
- path('users//nodes//assets/', api.UserGrantedNodeAssetsApi.as_view(), name='user-node-assets'),
- path('users/nodes//assets/', api.UserGrantedNodeAssetsApi.as_view(), name='my-node-assets'),
-
- # Asset System users
- path('users//assets//system-users/', api.UserGrantedAssetSystemUsersApi.as_view(), name='user-asset-system-users'),
- path('users/assets//system-users/', api.UserGrantedAssetSystemUsersApi.as_view(), name='my-asset-system-users'),
-
- # 查询某个用户组授权的资产和资产组
- path('user-groups//assets/', api.UserGroupGrantedAssetsApi.as_view(), name='user-group-assets'),
- path('user-groups//nodes/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes'),
- path('user-groups//nodes/children/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes-children'),
- path('user-groups//nodes/children/tree/', api.UserGroupGrantedNodeChildrenAsTreeApi.as_view(), name='user-group-nodes-children-as-tree'),
- path('user-groups//nodes//assets/', api.UserGroupGrantedNodeAssetsApi.as_view(), name='user-group-node-assets'),
- path('user-groups//assets//system-users/', api.UserGroupGrantedAssetSystemUsersApi.as_view(), name='user-group-asset-system-users'),
-
- # 用户和资产授权变更
- path('asset-permissions//users/remove/', api.AssetPermissionRemoveUserApi.as_view(), name='asset-permission-remove-user'),
- path('asset-permissions//users/add/', api.AssetPermissionAddUserApi.as_view(), name='asset-permission-add-user'),
- path('asset-permissions//assets/remove/', api.AssetPermissionRemoveAssetApi.as_view(), name='asset-permission-remove-asset'),
- path('asset-permissions//assets/add/', api.AssetPermissionAddAssetApi.as_view(), name='asset-permission-add-asset'),
-
- # 授权规则中授权的资产
- path('asset-permissions//assets/', api.AssetPermissionAssetsApi.as_view(), name='asset-permission-assets'),
-
- # 验证用户是否有某个资产和系统用户的权限
- path('asset-permissions/user/validate/', api.ValidateUserAssetPermissionApi.as_view(), name='validate-user-asset-permission'),
- path('asset-permissions/user/actions/', api.GetUserAssetPermissionActionsApi.as_view(), name='get-user-asset-permission-actions'),
-
- # 刷新缓存
- path('asset-permissions/cache/refresh/', api.RefreshAssetPermissionCacheApi.as_view(), name='refresh-asset-permission-cache'),
-]
-
-
-remote_app_permission_urlpatterns = [
- # 查询用户授权的RemoteApp
- path('users//remote-apps/', api.UserGrantedRemoteAppsApi.as_view(), name='user-remote-apps'),
- path('users/remote-apps/', api.UserGrantedRemoteAppsApi.as_view(), name='my-remote-apps'),
-
- # 获取用户授权的RemoteApp树
- path('users//remote-apps/tree/', api.UserGrantedRemoteAppsAsTreeApi.as_view(), name='user-remote-apps-as-tree'),
- path('users/remote-apps/tree/', api.UserGrantedRemoteAppsAsTreeApi.as_view(), name='my-remote-apps-as-tree'),
-
- # 查询用户组授权的RemoteApp
- path('user-groups//remote-apps/', api.UserGroupGrantedRemoteAppsApi.as_view(), name='user-group-remote-apps'),
-
- # RemoteApp System users
- path('users//remote-apps//system-users/', api.UserGrantedRemoteAppSystemUsersApi.as_view(), name='user-remote-app-system-users'),
- path('users/remote-apps//system-users/', api.UserGrantedRemoteAppSystemUsersApi.as_view(), name='my-remote-app-system-users'),
-
- # 校验用户对RemoteApp的权限
- path('remote-app-permissions/user/validate/', api.ValidateUserRemoteAppPermissionApi.as_view(), name='validate-user-remote-app-permission'),
-
- # 用户和RemoteApp变更
- path('remote-app-permissions//users/add/', api.RemoteAppPermissionAddUserApi.as_view(), name='remote-app-permission-add-user'),
- path('remote-app-permissions//users/remove/', api.RemoteAppPermissionRemoveUserApi.as_view(), name='remote-app-permission-remove-user'),
- path('remote-app-permissions//remote-apps/remove/', api.RemoteAppPermissionRemoveRemoteAppApi.as_view(), name='remote-app-permission-remove-remote-app'),
- path('remote-app-permissions//remote-apps/add/', api.RemoteAppPermissionAddRemoteAppApi.as_view(), name='remote-app-permission-add-remote-app'),
-]
old_version_urlpatterns = [
re_path('(?Puser|user-group|asset-permission|remote-app-permission)/.*', capi.redirect_plural_name_api)
]
-urlpatterns = asset_permission_urlpatterns + remote_app_permission_urlpatterns + old_version_urlpatterns
-
-urlpatterns += router.urls
+urlpatterns = asset_permission_urlpatterns + \
+ remote_app_permission_urlpatterns + \
+ database_app_permission_urlpatterns + \
+ old_version_urlpatterns
diff --git a/apps/perms/urls/asset_permission.py b/apps/perms/urls/asset_permission.py
new file mode 100644
index 000000000..4a649c992
--- /dev/null
+++ b/apps/perms/urls/asset_permission.py
@@ -0,0 +1,86 @@
+# coding:utf-8
+
+from django.urls import path, include
+from rest_framework_bulk.routes import BulkRouter
+from .. import api
+
+router = BulkRouter()
+router.register('asset-permissions', api.AssetPermissionViewSet, 'asset-permission')
+router.register('asset-permissions-users-relations', api.AssetPermissionUserRelationViewSet, 'asset-permissions-users-relation')
+router.register('asset-permissions-user-groups-relations', api.AssetPermissionUserGroupRelationViewSet, 'asset-permissions-user-groups-relation')
+router.register('asset-permissions-assets-relations', api.AssetPermissionAssetRelationViewSet, 'asset-permissions-assets-relation')
+router.register('asset-permissions-nodes-relations', api.AssetPermissionNodeRelationViewSet, 'asset-permissions-nodes-relation')
+router.register('asset-permissions-system-users-relations', api.AssetPermissionSystemUserRelationViewSet, 'asset-permissions-system-users-relation')
+
+user_permission_urlpatterns = [
+ path('/assets/', api.UserGrantedAssetsApi.as_view(), name='user-assets'),
+ path('assets/', api.UserGrantedAssetsApi.as_view(), name='my-assets'),
+
+ # Assets as tree
+ path('/assets/tree/', api.UserGrantedAssetsAsTreeApi.as_view(), name='user-assets-as-tree'),
+ path('assets/tree/', api.UserGrantedAssetsAsTreeApi.as_view(), name='my-assets-as-tree'),
+
+ # Nodes
+ path('/nodes/', api.UserGrantedNodesApi.as_view(), name='user-nodes'),
+ path('nodes/', api.UserGrantedNodesApi.as_view(), name='my-nodes'),
+
+ # Node children
+ path('/nodes/children/', api.UserGrantedNodesApi.as_view(), name='user-nodes-children'),
+ path('nodes/children/', api.UserGrantedNodesApi.as_view(), name='my-nodes-children'),
+
+ # Node as tree
+ path('/nodes/tree/', api.UserGrantedNodesAsTreeApi.as_view(), name='user-nodes-as-tree'),
+ path('nodes/tree/', api.UserGrantedNodesAsTreeApi.as_view(), name='my-nodes-as-tree'),
+
+ # Node with assets as tree
+ path('/nodes-with-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='user-nodes-with-assets-as-tree'),
+ path('nodes-with-assets/tree/', api.UserGrantedNodesWithAssetsAsTreeApi.as_view(), name='my-nodes-with-assets-as-tree'),
+
+ # Node children as tree
+ path('/nodes/children/tree/', api.UserGrantedNodeChildrenAsTreeApi.as_view(), name='user-nodes-children-as-tree'),
+ path('nodes/children/tree/', api.UserGrantedNodeChildrenAsTreeApi.as_view(), name='my-nodes-children-as-tree'),
+
+ # Node children with assets as tree
+ path('/nodes/children-with-assets/tree/', api.UserGrantedNodeChildrenWithAssetsAsTreeApi.as_view(), name='user-nodes-children-with-assets-as-tree'),
+ path('nodes/children-with-assets/tree/', api.UserGrantedNodeChildrenWithAssetsAsTreeApi.as_view(), name='my-nodes-children-with-assets-as-tree'),
+
+ # Node assets
+ path('/nodes//assets/', api.UserGrantedNodeAssetsApi.as_view(), name='user-node-assets'),
+ path('nodes//assets/', api.UserGrantedNodeAssetsApi.as_view(), name='my-node-assets'),
+
+ # Asset System users
+ path('/assets//system-users/', api.UserGrantedAssetSystemUsersApi.as_view(), name='user-asset-system-users'),
+ path('assets//system-users/', api.UserGrantedAssetSystemUsersApi.as_view(), name='my-asset-system-users'),
+]
+
+user_group_permission_urlpatterns = [
+ # 查询某个用户组授权的资产和资产组
+ path('/assets/', api.UserGroupGrantedAssetsApi.as_view(), name='user-group-assets'),
+ path('/nodes/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes'),
+ path('/nodes/children/', api.UserGroupGrantedNodesApi.as_view(), name='user-group-nodes-children'),
+ path('/nodes/children/tree/', api.UserGroupGrantedNodeChildrenAsTreeApi.as_view(), name='user-group-nodes-children-as-tree'),
+ path('/nodes//assets/', api.UserGroupGrantedNodeAssetsApi.as_view(), name='user-group-node-assets'),
+ path('/assets//system-users/', api.UserGroupGrantedAssetSystemUsersApi.as_view(), name='user-group-asset-system-users'),
+]
+
+permission_urlpatterns = [
+ # 授权规则中授权的资产
+ path('/assets/all/', api.AssetPermissionAllAssetListApi.as_view(), name='asset-permission-all-assets'),
+ path('/users/all/', api.AssetPermissionAllUserListApi.as_view(), name='asset-permission-all-users'),
+
+ # 验证用户是否有某个资产和系统用户的权限
+ path('user/validate/', api.ValidateUserAssetPermissionApi.as_view(), name='validate-user-asset-permission'),
+ path('user/actions/', api.GetUserAssetPermissionActionsApi.as_view(), name='get-user-asset-permission-actions'),
+
+ # 刷新缓存
+ path('cache/refresh/', api.RefreshAssetPermissionCacheApi.as_view(), name='refresh-asset-permission-cache'),
+]
+
+asset_permission_urlpatterns = [
+ # Assets
+ path('users/', include(user_permission_urlpatterns)),
+ path('user-groups/', include(user_group_permission_urlpatterns)),
+ path('asset-permissions/', include(permission_urlpatterns)),
+]
+
+asset_permission_urlpatterns += router.urls
diff --git a/apps/perms/urls/database_app_permission.py b/apps/perms/urls/database_app_permission.py
new file mode 100644
index 000000000..a793f980e
--- /dev/null
+++ b/apps/perms/urls/database_app_permission.py
@@ -0,0 +1,47 @@
+# coding: utf-8
+#
+
+from django.urls import path, include
+from rest_framework_bulk.routes import BulkRouter
+from .. import api
+
+
+router = BulkRouter()
+router.register('database-app-permissions', api.DatabaseAppPermissionViewSet, 'database-app-permission')
+router.register('database-app-permissions-users-relations', api.DatabaseAppPermissionUserRelationViewSet, 'database-app-permissions-users-relation')
+router.register('database-app-permissions-user-groups-relations', api.DatabaseAppPermissionUserGroupRelationViewSet, 'database-app-permissions-user-groups-relation')
+router.register('database-app-permissions-database-apps-relations', api.DatabaseAppPermissionDatabaseAppRelationViewSet, 'database-app-permissions-database-apps-relation')
+router.register('database-app-permissions-system-users-relations', api.DatabaseAppPermissionSystemUserRelationViewSet, 'database-app-permissions-system-users-relation')
+
+user_permission_urlpatterns = [
+ path('/database-apps/', api.UserGrantedDatabaseAppsApi.as_view(), name='user-database-apps'),
+ path('database-apps/', api.UserGrantedDatabaseAppsApi.as_view(), name='my-database-apps'),
+
+ # DatabaseApps as tree
+ path('/database-apps/tree/', api.UserGrantedDatabaseAppsAsTreeApi.as_view(), name='user-databases-apps-tree'),
+ path('database-apps/tree/', api.UserGrantedDatabaseAppsAsTreeApi.as_view(), name='my-databases-apps-tree'),
+
+ path('/database-apps//system-users/', api.UserGrantedDatabaseAppSystemUsersApi.as_view(), name='user-database-app-system-users'),
+ path('database-apps//system-users/', api.UserGrantedDatabaseAppSystemUsersApi.as_view(), name='user-database-app-system-users'),
+]
+
+user_group_permission_urlpatterns = [
+ path('/database-apps/', api.UserGroupGrantedDatabaseAppsApi.as_view(), name='user-group-database-apps'),
+]
+
+permission_urlpatterns = [
+ # 授权规则中授权的用户和数据库应用
+ path('/users/all/', api.DatabaseAppPermissionAllUserListApi.as_view(), name='database-app-permission-all-users'),
+ path('/database-apps/all/', api.DatabaseAppPermissionAllDatabaseAppListApi.as_view(), name='database-app-permission-all-database-apps'),
+
+ # 验证用户是否有某个数据库应用的权限
+ path('user/validate/', api.ValidateUserDatabaseAppPermissionApi.as_view(), name='validate-user-database-app-permission'),
+]
+
+database_app_permission_urlpatterns = [
+ path('users/', include(user_permission_urlpatterns)),
+ path('user-groups/', include(user_group_permission_urlpatterns)),
+ path('database-app-permissions/', include(permission_urlpatterns))
+]
+
+database_app_permission_urlpatterns += router.urls
diff --git a/apps/perms/urls/remote_app_permission.py b/apps/perms/urls/remote_app_permission.py
new file mode 100644
index 000000000..8f83d72d0
--- /dev/null
+++ b/apps/perms/urls/remote_app_permission.py
@@ -0,0 +1,38 @@
+# coding:utf-8
+
+from django.urls import path
+from rest_framework_bulk.routes import BulkRouter
+from .. import api
+
+
+router = BulkRouter()
+router.register('remote-app-permissions', api.RemoteAppPermissionViewSet, 'remote-app-permission')
+
+remote_app_permission_urlpatterns = [
+ # 查询用户授权的RemoteApp
+ path('users//remote-apps/', api.UserGrantedRemoteAppsApi.as_view(), name='user-remote-apps'),
+ path('users/remote-apps/', api.UserGrantedRemoteAppsApi.as_view(), name='my-remote-apps'),
+
+ # 获取用户授权的RemoteApp树
+ path('users//remote-apps/tree/', api.UserGrantedRemoteAppsAsTreeApi.as_view(), name='user-remote-apps-as-tree'),
+ path('users/remote-apps/tree/', api.UserGrantedRemoteAppsAsTreeApi.as_view(), name='my-remote-apps-as-tree'),
+
+ # 查询用户组授权的RemoteApp
+ path('user-groups//remote-apps/', api.UserGroupGrantedRemoteAppsApi.as_view(), name='user-group-remote-apps'),
+
+ # RemoteApp System users
+ path('users//remote-apps//system-users/', api.UserGrantedRemoteAppSystemUsersApi.as_view(), name='user-remote-app-system-users'),
+ path('users/remote-apps//system-users/', api.UserGrantedRemoteAppSystemUsersApi.as_view(), name='my-remote-app-system-users'),
+
+ # 校验用户对RemoteApp的权限
+ path('remote-app-permissions/user/validate/', api.ValidateUserRemoteAppPermissionApi.as_view(), name='validate-user-remote-app-permission'),
+
+ # 用户和RemoteApp变更
+ path('remote-app-permissions//users/add/', api.RemoteAppPermissionAddUserApi.as_view(), name='remote-app-permission-add-user'),
+ path('remote-app-permissions//users/remove/', api.RemoteAppPermissionRemoveUserApi.as_view(), name='remote-app-permission-remove-user'),
+ path('remote-app-permissions//remote-apps/remove/', api.RemoteAppPermissionRemoveRemoteAppApi.as_view(), name='remote-app-permission-remove-remote-app'),
+ path('remote-app-permissions//remote-apps/add/', api.RemoteAppPermissionAddRemoteAppApi.as_view(), name='remote-app-permission-add-remote-app'),
+]
+
+remote_app_permission_urlpatterns += router.urls
+
diff --git a/apps/perms/urls/views_urls.py b/apps/perms/urls/views_urls.py
index 964025db3..f4deba8c8 100644
--- a/apps/perms/urls/views_urls.py
+++ b/apps/perms/urls/views_urls.py
@@ -23,4 +23,12 @@ urlpatterns = [
path('remote-app-permission//', views.RemoteAppPermissionDetailView.as_view(), name='remote-app-permission-detail'),
path('remote-app-permission//user/', views.RemoteAppPermissionUserView.as_view(), name='remote-app-permission-user-list'),
path('remote-app-permission//remote-app/', views.RemoteAppPermissionRemoteAppView.as_view(), name='remote-app-permission-remote-app-list'),
+
+ # database-app-permission
+ path('database-app-permission/', views.DatabaseAppPermissionListView.as_view(), name='database-app-permission-list'),
+ path('database-app-permission/create/', views.DatabaseAppPermissionCreateView.as_view(), name='database-app-permission-create'),
+ path('database-app-permission//update/', views.DatabaseAppPermissionUpdateView.as_view(), name='database-app-permission-update'),
+ path('database-app-permission//', views.DatabaseAppPermissionDetailView.as_view(), name='database-app-permission-detail'),
+ path('database-app-permission//user/', views.DatabaseAppPermissionUserView.as_view(), name='database-app-permission-user-list'),
+ path('database-app-permission//database-app/', views.DatabaseAppPermissionDatabaseAppView.as_view(), name='database-app-permission-database-app-list'),
]
diff --git a/apps/perms/utils/__init__.py b/apps/perms/utils/__init__.py
index 129901afc..c6581b858 100644
--- a/apps/perms/utils/__init__.py
+++ b/apps/perms/utils/__init__.py
@@ -3,3 +3,4 @@
from .asset_permission import *
from .remote_app_permission import *
+from .database_app_permission import *
diff --git a/apps/perms/utils/asset_permission.py b/apps/perms/utils/asset_permission.py
index f71f83663..27c839d59 100644
--- a/apps/perms/utils/asset_permission.py
+++ b/apps/perms/utils/asset_permission.py
@@ -437,7 +437,7 @@ def sort_assets(assets, order_by='hostname', reverse=False):
class ParserNode:
nodes_only_fields = ("key", "value", "id")
- assets_only_fields = ("platform", "hostname", "id", "ip", "protocols")
+ assets_only_fields = ("hostname", "id", "ip", "protocols", "org_id")
system_users_only_fields = (
"id", "name", "username", "protocol", "priority", "login_mode",
)
@@ -445,7 +445,6 @@ class ParserNode:
@staticmethod
def parse_node_to_tree_node(node):
name = '{} ({})'.format(node.value, node.assets_amount)
- # name = node.value
data = {
'id': node.key,
'name': name,
@@ -468,7 +467,7 @@ class ParserNode:
@staticmethod
def parse_asset_to_tree_node(node, asset):
icon_skin = 'file'
- platform = asset.platform.lower()
+ platform = asset.platform_base.lower()
if platform == 'windows':
icon_skin = 'windows'
elif platform == 'linux':
@@ -489,8 +488,8 @@ class ParserNode:
'hostname': asset.hostname,
'ip': asset.ip,
'protocols': asset.protocols_as_list,
- 'platform': asset.platform,
- "org_name": asset.org_name,
+ 'platform': asset.platform_base,
+ 'org_name': asset.org_name,
},
}
}
diff --git a/apps/perms/utils/database_app_permission.py b/apps/perms/utils/database_app_permission.py
new file mode 100644
index 000000000..526b0fc59
--- /dev/null
+++ b/apps/perms/utils/database_app_permission.py
@@ -0,0 +1,99 @@
+# coding: utf-8
+#
+
+from django.db.models import Q
+from orgs.utils import set_to_root_org
+
+from ..models import DatabaseAppPermission
+from common.tree import TreeNode
+from applications.models import DatabaseApp
+from assets.models import SystemUser
+
+
+__all__ = [
+ 'DatabaseAppPermissionUtil',
+ 'construct_database_apps_tree_root',
+ 'parse_database_app_to_tree_node'
+]
+
+
+def get_user_database_app_permissions(user, include_group=True):
+ if include_group:
+ groups = user.groups.all()
+ arg = Q(users=user) | Q(user_groups__in=groups)
+ else:
+ arg = Q(users=user)
+ return DatabaseAppPermission.objects.all().valid().filter(arg)
+
+
+def get_user_group_database_app_permission(user_group):
+ return DatabaseAppPermission.objects.all().valid().filter(
+ user_group=user_group
+ )
+
+
+class DatabaseAppPermissionUtil:
+ get_permissions_map = {
+ 'User': get_user_database_app_permissions,
+ 'UserGroup': get_user_group_database_app_permission
+ }
+
+ def __init__(self, obj):
+ self.object = obj
+ self.change_org_if_need()
+
+ @staticmethod
+ def change_org_if_need():
+ set_to_root_org()
+
+ @property
+ def permissions(self):
+ obj_class = self.object.__class__.__name__
+ func = self.get_permissions_map[obj_class]
+ _permissions = func(self.object)
+ return _permissions
+
+ def get_database_apps(self):
+ database_apps = DatabaseApp.objects.filter(
+ granted_by_permissions__in=self.permissions
+ )
+ return database_apps
+
+ def get_database_app_system_users(self, database_app):
+ queryset = self.permissions
+ kwargs = {'database_apps': database_app}
+ queryset = queryset.filter(**kwargs)
+ system_users_ids = queryset.values_list('system_users', flat=True)
+ system_users_ids = system_users_ids.distinct()
+ system_users = SystemUser.objects.filter(id__in=system_users_ids)
+ system_users = system_users.order_by('-priority')
+ return system_users
+
+
+def construct_database_apps_tree_root():
+ tree_root = {
+ 'id': 'ID_DATABASE_APP_ROOT',
+ 'name': 'DatabaseApp',
+ 'title': 'DatabaseApp',
+ 'pId': '',
+ 'open': False,
+ 'isParent': True,
+ 'iconSkin': '',
+ 'meta': {'type': 'database_app'}
+ }
+ return TreeNode(**tree_root)
+
+
+def parse_database_app_to_tree_node(parent, database_app):
+ pid = parent.id if parent else ''
+ tree_node = {
+ 'id': database_app.id,
+ 'name': database_app.name,
+ 'title': database_app.name,
+ 'pId': pid,
+ 'open': False,
+ 'isParent': False,
+ 'iconSkin': 'file',
+ 'meta': {'type': 'database_app'}
+ }
+ return TreeNode(**tree_node)
diff --git a/apps/perms/views/__init__.py b/apps/perms/views/__init__.py
index 129901afc..c6581b858 100644
--- a/apps/perms/views/__init__.py
+++ b/apps/perms/views/__init__.py
@@ -3,3 +3,4 @@
from .asset_permission import *
from .remote_app_permission import *
+from .database_app_permission import *
diff --git a/apps/perms/views/asset_permission.py b/apps/perms/views/asset_permission.py
index 6130ad2e3..71b4d5604 100644
--- a/apps/perms/views/asset_permission.py
+++ b/apps/perms/views/asset_permission.py
@@ -98,9 +98,7 @@ class AssetPermissionDetailView(PermissionsMixin, DetailView):
context = {
'app': _('Perms'),
'action': _('Asset permission detail'),
- 'system_users_remain': SystemUser.objects.exclude(
- granted_by_permissions=self.object
- ),
+
}
kwargs.update(context)
return super().get_context_data(**kwargs)
@@ -131,14 +129,13 @@ class AssetPermissionUserView(PermissionsMixin,
return queryset
def get_context_data(self, **kwargs):
- user_remain = current_org.get_org_members(exclude=('Auditor',)).exclude(
- assetpermission=self.object)
+ users = [str(i) for i in self.object.users.all().values_list('id', flat=True)]
user_groups_remain = UserGroup.objects.exclude(
assetpermission=self.object)
context = {
'app': _('Perms'),
'action': _('Asset permission user list'),
- 'users_remain': user_remain,
+ 'users': users,
'user_groups_remain': user_groups_remain,
}
kwargs.update(context)
@@ -163,13 +160,16 @@ class AssetPermissionAssetView(PermissionsMixin,
return queryset
def get_context_data(self, **kwargs):
- nodes_remain = Node.objects.exclude(
- id__in=self.object.nodes.all().values_list('id', flat=True)
- ).only('key')
+ assets = self.object.assets.all().values_list('id', flat=True)
+ assets = [str(i) for i in assets]
+ system_users_remain = SystemUser.objects\
+ .exclude(granted_by_permissions=self.object)\
+ .exclude(protocol=SystemUser.PROTOCOL_MYSQL)
context = {
'app': _('Perms'),
+ 'assets': assets,
'action': _('Asset permission asset list'),
- 'nodes_remain': nodes_remain,
+ 'system_users_remain': system_users_remain,
}
kwargs.update(context)
- return super().get_context_data(**kwargs)
\ No newline at end of file
+ return super().get_context_data(**kwargs)
diff --git a/apps/perms/views/database_app_permission.py b/apps/perms/views/database_app_permission.py
new file mode 100644
index 000000000..50627defe
--- /dev/null
+++ b/apps/perms/views/database_app_permission.py
@@ -0,0 +1,152 @@
+# coding: utf-8
+#
+
+from django.utils.translation import ugettext as _
+
+from django.views.generic import (
+ TemplateView, CreateView, UpdateView, DetailView, ListView
+)
+from django.views.generic.edit import SingleObjectMixin
+from django.conf import settings
+
+from common.permissions import PermissionsMixin, IsOrgAdmin
+from users.models import UserGroup
+from applications.models import DatabaseApp
+from assets.models import SystemUser
+
+from .. import models, forms
+
+
+__all__ = [
+ 'DatabaseAppPermissionListView', 'DatabaseAppPermissionCreateView',
+ 'DatabaseAppPermissionUpdateView', 'DatabaseAppPermissionDetailView',
+ 'DatabaseAppPermissionUserView', 'DatabaseAppPermissionDatabaseAppView',
+]
+
+
+class DatabaseAppPermissionListView(PermissionsMixin, TemplateView):
+ template_name = 'perms/database_app_permission_list.html'
+ permission_classes = [IsOrgAdmin]
+
+ def get_context_data(self, **kwargs):
+ context = {
+ 'app': _('Perms'),
+ 'action': _('DatabaseApp permission list')
+ }
+ kwargs.update(context)
+ return super().get_context_data(**kwargs)
+
+
+class DatabaseAppPermissionCreateView(PermissionsMixin, CreateView):
+ template_name = 'perms/database_app_permission_create_update.html'
+ model = models.DatabaseAppPermission
+ form_class = forms.DatabaseAppPermissionCreateUpdateForm
+ permission_classes = [IsOrgAdmin]
+
+ def get_context_data(self, **kwargs):
+ context = {
+ 'app': _('Perms'),
+ 'action': _('Create DatabaseApp permission'),
+ 'api_action': 'create',
+ }
+ kwargs.update(context)
+ return super().get_context_data(**kwargs)
+
+
+class DatabaseAppPermissionUpdateView(PermissionsMixin, UpdateView):
+ template_name = 'perms/database_app_permission_create_update.html'
+ model = models.DatabaseAppPermission
+ form_class = forms.DatabaseAppPermissionCreateUpdateForm
+ permission_classes = [IsOrgAdmin]
+
+ def get_context_data(self, **kwargs):
+ context = {
+ 'app': _('Perms'),
+ 'action': _('Update DatabaseApp permission'),
+ 'api_action': 'update'
+ }
+ kwargs.update(context)
+ return super().get_context_data(**kwargs)
+
+
+class DatabaseAppPermissionDetailView(PermissionsMixin, DetailView):
+ template_name = 'perms/database_app_permission_detail.html'
+ model = models.DatabaseAppPermission
+ permission_classes = [IsOrgAdmin]
+
+ def get_context_data(self, **kwargs):
+ context = {
+ 'app': _('Perms'),
+ 'action': _('DatabaseApp permission detail')
+ }
+ kwargs.update(context)
+ return super().get_context_data(**kwargs)
+
+
+class DatabaseAppPermissionUserView(PermissionsMixin,
+ SingleObjectMixin,
+ ListView):
+ template_name = 'perms/database_app_permission_user.html'
+ context_object_name = 'database_app_permission'
+ paginate_by = settings.DISPLAY_PER_PAGE
+ object = None
+ permission_classes = [IsOrgAdmin]
+
+ def get(self, request, *args, **kwargs):
+ self.object = self.get_object(queryset=models.DatabaseAppPermission.objects.all())
+ return super().get(request, *args, **kwargs)
+
+ def get_queryset(self):
+ queryset = list(self.object.get_all_users())
+ return queryset
+
+ def get_context_data(self, **kwargs):
+ users = [str(i) for i in self.object.users.all().values_list('id', flat=True)]
+ user_groups_remain = UserGroup.objects.exclude(
+ databaseapppermission=self.object)
+ context = {
+ 'app': _('Perms'),
+ 'action': _('DatabaseApp permission user list'),
+ 'users': users,
+ 'user_groups_remain': user_groups_remain,
+ }
+ kwargs.update(context)
+ return super().get_context_data(**kwargs)
+
+
+class DatabaseAppPermissionDatabaseAppView(PermissionsMixin,
+ SingleObjectMixin,
+ ListView):
+ template_name = 'perms/database_app_permission_database_app.html'
+ context_object_name = 'database_app_permission'
+ paginate_by = settings.DISPLAY_PER_PAGE
+ object = None
+ permission_classes = [IsOrgAdmin]
+
+ def get(self, request, *args, **kwargs):
+ self.object = self.get_object(
+ queryset=models.DatabaseAppPermission.objects.all()
+ )
+ return super().get(request, *args, **kwargs)
+
+ def get_queryset(self):
+ queryset = list(self.object.get_all_database_apps())
+ return queryset
+
+ def get_context_data(self, **kwargs):
+ database_apps = self.object.get_all_database_apps().values_list('id', flat=True)
+ database_apps = [str(i) for i in database_apps]
+ system_users_remain = SystemUser.objects\
+ .exclude(granted_by_database_app_permissions=self.object)\
+ .filter(protocol=SystemUser.PROTOCOL_MYSQL)
+ context = {
+ 'app': _('Perms'),
+ 'database_apps': database_apps,
+ 'database_apps_remain': DatabaseApp.objects.exclude(
+ granted_by_permissions=self.object
+ ),
+ 'system_users_remain': system_users_remain,
+ 'action': _('DatabaseApp permission DatabaseApp list'),
+ }
+ kwargs.update(context)
+ return super().get_context_data(**kwargs)
diff --git a/apps/perms/views/remote_app_permission.py b/apps/perms/views/remote_app_permission.py
index 742a5fd85..875600c68 100644
--- a/apps/perms/views/remote_app_permission.py
+++ b/apps/perms/views/remote_app_permission.py
@@ -48,7 +48,7 @@ class RemoteAppPermissionCreateView(PermissionsMixin, CreateView):
context = {
'app': _('Perms'),
'action': _('Create RemoteApp permission'),
- 'type': 'create'
+ 'api_action': 'create'
}
kwargs.update(context)
return super().get_context_data(**kwargs)
@@ -65,7 +65,7 @@ class RemoteAppPermissionUpdateView(PermissionsMixin, UpdateView):
context = {
'app': _('Perms'),
'action': _('Update RemoteApp permission'),
- 'type': 'update'
+ 'api_action': 'update'
}
kwargs.update(context)
return super().get_context_data(**kwargs)
@@ -77,12 +77,13 @@ class RemoteAppPermissionDetailView(PermissionsMixin, DetailView):
permission_classes = [IsOrgAdmin]
def get_context_data(self, **kwargs):
+ system_users_remain = SystemUser.objects\
+ .exclude(granted_by_remote_app_permissions=self.object)\
+ .filter(protocol=SystemUser.PROTOCOL_RDP)
context = {
'app': _('Perms'),
'action': _('RemoteApp permission detail'),
- 'system_users_remain': SystemUser.objects.exclude(
- granted_by_remote_app_permissions=self.object
- ),
+ 'system_users_remain': system_users_remain,
}
kwargs.update(context)
return super().get_context_data(**kwargs)
@@ -107,10 +108,10 @@ class RemoteAppPermissionUserView(PermissionsMixin,
return queryset
def get_context_data(self, **kwargs):
- user_remain = current_org.get_org_members(exclude=('Auditor',)).exclude(
- remoteapppermission=self.object)
- user_groups_remain = UserGroup.objects.exclude(
- remoteapppermission=self.object)
+ user_remain = current_org.get_org_members(exclude=('Auditor',))\
+ .exclude(remoteapppermission=self.object)
+ user_groups_remain = UserGroup.objects\
+ .exclude(remoteapppermission=self.object)
context = {
'app': _('Perms'),
'action': _('RemoteApp permission user list'),
diff --git a/apps/settings/api.py b/apps/settings/api.py
index 1c65dc12c..354cbdd1d 100644
--- a/apps/settings/api.py
+++ b/apps/settings/api.py
@@ -245,87 +245,6 @@ class LDAPCacheRefreshAPI(generics.RetrieveAPIView):
return Response(data={'msg': 'success'})
-class ReplayStorageCreateAPI(APIView):
- permission_classes = (IsSuperUser,)
-
- def post(self, request):
- storage_data = request.data
-
- if storage_data.get('TYPE') == 'ceph':
- port = storage_data.get('PORT')
- if port.isdigit():
- storage_data['PORT'] = int(storage_data.get('PORT'))
-
- storage_name = storage_data.pop('NAME')
- data = {storage_name: storage_data}
-
- if not self.is_valid(storage_data):
- return Response({
- "error": _("Error: Account invalid (Please make sure the "
- "information such as Access key or Secret key is correct)")},
- status=401
- )
-
- Setting.save_storage('TERMINAL_REPLAY_STORAGE', data)
- return Response({"msg": _('Create succeed')}, status=200)
-
- @staticmethod
- def is_valid(storage_data):
- if storage_data.get('TYPE') == 'server':
- return True
- storage = jms_storage.get_object_storage(storage_data)
- target = 'tests.py'
- src = os.path.join(settings.BASE_DIR, 'common', target)
- return storage.is_valid(src, target)
-
-
-class ReplayStorageDeleteAPI(APIView):
- permission_classes = (IsSuperUser,)
-
- def post(self, request):
- storage_name = str(request.data.get('name'))
- Setting.delete_storage('TERMINAL_REPLAY_STORAGE', storage_name)
- return Response({"msg": _('Delete succeed')}, status=200)
-
-
-class CommandStorageCreateAPI(APIView):
- permission_classes = (IsSuperUser,)
-
- def post(self, request):
- storage_data = request.data
- storage_name = storage_data.pop('NAME')
- data = {storage_name: storage_data}
- if not self.is_valid(storage_data):
- return Response(
- {"error": _("Error: Account invalid (Please make sure the "
- "information such as Access key or Secret key is correct)")},
- status=401
- )
-
- Setting.save_storage('TERMINAL_COMMAND_STORAGE', data)
- return Response({"msg": _('Create succeed')}, status=200)
-
- @staticmethod
- def is_valid(storage_data):
- if storage_data.get('TYPE') == 'server':
- return True
- try:
- storage = jms_storage.get_log_storage(storage_data)
- except Exception:
- return False
-
- return storage.ping()
-
-
-class CommandStorageDeleteAPI(APIView):
- permission_classes = (IsSuperUser,)
-
- def post(self, request):
- storage_name = str(request.data.get('name'))
- Setting.delete_storage('TERMINAL_COMMAND_STORAGE', storage_name)
- return Response({"msg": _('Delete succeed')}, status=200)
-
-
class PublicSettingApi(generics.RetrieveAPIView):
permission_classes = ()
serializer_class = PublicSettingSerializer
diff --git a/apps/settings/forms.py b/apps/settings/forms.py
deleted file mode 100644
index 5b55091cb..000000000
--- a/apps/settings/forms.py
+++ /dev/null
@@ -1,289 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-import json
-from django import forms
-from django.utils.translation import ugettext_lazy as _
-from django.db import transaction
-
-from .models import Setting, settings
-from common.fields import (
- FormDictField, FormEncryptCharField, FormEncryptMixin
-)
-
-
-class BaseForm(forms.Form):
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- for name, field in self.fields.items():
- value = getattr(settings, name, None)
- if value is None: # and django_value is None:
- continue
-
- if value is not None:
- if isinstance(value, dict):
- value = json.dumps(value)
- initial_value = value
- else:
- initial_value = ''
- field.initial = initial_value
-
- def save(self, category="default"):
- if not self.is_bound:
- raise ValueError("Form is not bound")
-
- # db_settings = Setting.objects.all()
- if not self.is_valid():
- raise ValueError(self.errors)
-
- with transaction.atomic():
- for name, value in self.cleaned_data.items():
- field = self.fields[name]
- if isinstance(field.widget, forms.PasswordInput) and not value:
- continue
- # if value == getattr(settings, name):
- # continue
-
- encrypted = True if isinstance(field, FormEncryptMixin) else False
- try:
- setting = Setting.objects.get(name=name)
- except Setting.DoesNotExist:
- setting = Setting()
- setting.name = name
- setting.category = category
- setting.encrypted = encrypted
- setting.cleaned_value = value
- setting.save()
-
-
-class BasicSettingForm(BaseForm):
- SITE_URL = forms.URLField(
- label=_("Current SITE URL"),
- help_text="eg: http://jumpserver.abc.com:8080"
- )
- USER_GUIDE_URL = forms.URLField(
- label=_("User Guide URL"), required=False,
- help_text=_("User first login update profile done redirect to it")
- )
- EMAIL_SUBJECT_PREFIX = forms.CharField(
- max_length=1024, label=_("Email Subject Prefix"),
- help_text=_("Tips: Some word will be intercept by mail provider")
- )
-
-
-class EmailSettingForm(BaseForm):
- EMAIL_HOST = forms.CharField(
- max_length=1024, label=_("SMTP host"), initial='smtp.jumpserver.org'
- )
- EMAIL_PORT = forms.CharField(max_length=5, label=_("SMTP port"), initial=25)
- EMAIL_HOST_USER = forms.CharField(
- max_length=128, label=_("SMTP user"), initial='noreply@jumpserver.org'
- )
- EMAIL_HOST_PASSWORD = FormEncryptCharField(
- max_length=1024, label=_("SMTP password"), widget=forms.PasswordInput,
- required=False,
- help_text=_("Tips: Some provider use token except password")
- )
- EMAIL_FROM = forms.CharField(
- max_length=128, label=_("Send user"), initial='', required=False,
- help_text=_(
- "Tips: Send mail account, default SMTP account as the send account"
- )
- )
- EMAIL_RECIPIENT = forms.CharField(
- max_length=128, label=_("Test recipient"), initial='', required=False,
- help_text=_("Tips: Used only as a test mail recipient")
- )
- EMAIL_USE_SSL = forms.BooleanField(
- label=_("Use SSL"), initial=False, required=False,
- help_text=_("If SMTP port is 465, may be select")
- )
- EMAIL_USE_TLS = forms.BooleanField(
- label=_("Use TLS"), initial=False, required=False,
- help_text=_("If SMTP port is 587, may be select")
- )
-
-
-class LDAPSettingForm(BaseForm):
- AUTH_LDAP_SERVER_URI = forms.CharField(
- label=_("LDAP server"),
- )
- AUTH_LDAP_BIND_DN = forms.CharField(
- required=False, label=_("Bind DN"),
- )
- AUTH_LDAP_BIND_PASSWORD = FormEncryptCharField(
- label=_("Password"),
- widget=forms.PasswordInput, required=False
- )
- AUTH_LDAP_SEARCH_OU = forms.CharField(
- label=_("User OU"),
- help_text=_("Use | split User OUs"),
- required=False,
- )
- AUTH_LDAP_SEARCH_FILTER = forms.CharField(
- label=_("User search filter"),
- help_text=_("Choice may be (cn|uid|sAMAccountName)=%(user)s)")
- )
- AUTH_LDAP_USER_ATTR_MAP = FormDictField(
- label=_("User attr map"),
- help_text=_(
- "User attr map present how to map LDAP user attr to jumpserver, "
- "username,name,email is jumpserver attr"
- ),
- )
- # AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU
- # AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER
- # AUTH_LDAP_START_TLS = forms.BooleanField(
- # label=_("Use SSL"), required=False
- # )
- AUTH_LDAP = forms.BooleanField(label=_("Enable LDAP auth"), required=False)
-
-
-class TerminalSettingForm(BaseForm):
- SORT_BY_CHOICES = (
- ('hostname', _('Hostname')),
- ('ip', _('IP')),
- )
- PAGE_SIZE_CHOICES = (
- ('all', _('All')),
- ('auto', _('Auto')),
- (10, 10),
- (15, 15),
- (25, 25),
- (50, 50),
- )
- TERMINAL_PASSWORD_AUTH = forms.BooleanField(
- required=False, label=_("Password auth")
- )
- TERMINAL_PUBLIC_KEY_AUTH = forms.BooleanField(
- required=False, label=_("Public key auth")
- )
- TERMINAL_HEARTBEAT_INTERVAL = forms.IntegerField(
- min_value=5, max_value=99999, label=_("Heartbeat interval"),
- help_text=_("Units: seconds")
- )
- TERMINAL_ASSET_LIST_SORT_BY = forms.ChoiceField(
- choices=SORT_BY_CHOICES, label=_("List sort by")
- )
- TERMINAL_ASSET_LIST_PAGE_SIZE = forms.ChoiceField(
- choices=PAGE_SIZE_CHOICES, label=_("List page size"),
- )
- TERMINAL_SESSION_KEEP_DURATION = forms.IntegerField(
- min_value=1, max_value=99999, label=_("Session keep duration"),
- help_text=_("Units: days, Session, record, command will be delete "
- "if more than duration, only in database")
- )
- TERMINAL_TELNET_REGEX = forms.CharField(
- required=False, label=_("Telnet login regex"),
- help_text=_("ex: Last\s*login|success|成功")
- )
-
-
-class TerminalCommandStorage(BaseForm):
- pass
-
-
-class SecuritySettingForm(BaseForm):
- # MFA global setting
- SECURITY_MFA_AUTH = forms.BooleanField(
- required=False, label=_("MFA Secondary certification"),
- help_text=_(
- 'After opening, the user login must use MFA secondary '
- 'authentication (valid for all users, including administrators)'
- )
- )
- # Execute commands for user
- SECURITY_COMMAND_EXECUTION = forms.BooleanField(
- required=False, label=_("Batch execute commands"),
- help_text=_("Allow user batch execute commands")
- )
- SECURITY_SERVICE_ACCOUNT_REGISTRATION = forms.BooleanField(
- required=False, label=_("Service account registration"),
- help_text=_("Allow using bootstrap token register service account, "
- "when terminal setup, can disable it")
- )
- # limit login count
- SECURITY_LOGIN_LIMIT_COUNT = forms.IntegerField(
- min_value=3, max_value=99999,
- label=_("Limit the number of login failures")
- )
- # limit login time
- SECURITY_LOGIN_LIMIT_TIME = forms.IntegerField(
- min_value=5, max_value=99999, label=_("No logon interval"),
- help_text=_(
- "Tip: (unit/minute) if the user has failed to log in for a limited "
- "number of times, no login is allowed during this time interval."
- )
- )
- # ssh max idle time
- SECURITY_MAX_IDLE_TIME = forms.IntegerField(
- min_value=1, max_value=99999, required=False,
- label=_("Connection max idle time"),
- help_text=_(
- 'If idle time more than it, disconnect connection '
- 'Unit: minute'
- ),
- )
- # password expiration time
- SECURITY_PASSWORD_EXPIRATION_TIME = forms.IntegerField(
- min_value=1, max_value=99999, label=_("Password expiration time"),
- help_text=_(
- "Tip: (unit: day) "
- "If the user does not update the password during the time, "
- "the user password will expire failure;"
- "The password expiration reminder mail will be automatic sent to the user "
- "by system within 5 days (daily) before the password expires"
- )
- )
- # min length
- SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField(
- min_value=6, max_value=30, label=_("Password minimum length"),
- )
- # upper case
- SECURITY_PASSWORD_UPPER_CASE = forms.BooleanField(
- required=False, label=_("Must contain capital letters"),
- help_text=_(
- 'After opening, the user password changes '
- 'and resets must contain uppercase letters')
- )
- # lower case
- SECURITY_PASSWORD_LOWER_CASE = forms.BooleanField(
- required=False, label=_("Must contain lowercase letters"),
- help_text=_('After opening, the user password changes '
- 'and resets must contain lowercase letters')
- )
- # number
- SECURITY_PASSWORD_NUMBER = forms.BooleanField(
- required=False, label=_("Must contain numeric characters"),
- help_text=_('After opening, the user password changes '
- 'and resets must contain numeric characters')
- )
- # special char
- SECURITY_PASSWORD_SPECIAL_CHAR = forms.BooleanField(
- required=False, label=_("Must contain special characters"),
- help_text=_('After opening, the user password changes '
- 'and resets must contain special characters')
- )
-
-
-class EmailContentSettingForm(BaseForm):
- EMAIL_CUSTOM_USER_CREATED_SUBJECT = forms.CharField(
- max_length=1024, required=False, label=_("Create user email subject"),
- help_text=_("Tips: When creating a user, send the subject of the email"
- " (eg:Create account successfully)")
- )
- EMAIL_CUSTOM_USER_CREATED_HONORIFIC = forms.CharField(
- max_length=1024, required=False, label=_("Create user honorific"),
- help_text=_("Tips: When creating a user, send the honorific of the "
- "email (eg:Hello)")
- )
- EMAIL_CUSTOM_USER_CREATED_BODY = forms.CharField(
- max_length=4096, required=False, widget=forms.Textarea(),
- label=_('Create user email content'),
- help_text=_('Tips:When creating a user, send the content of the email')
- )
- EMAIL_CUSTOM_USER_CREATED_SIGNATURE = forms.CharField(
- max_length=512, required=False, label=_("Signature"),
- help_text=_("Tips: Email signature (eg:jumpserver)")
- )
-
-
diff --git a/apps/settings/forms/__init__.py b/apps/settings/forms/__init__.py
new file mode 100644
index 000000000..4c5a69e8c
--- /dev/null
+++ b/apps/settings/forms/__init__.py
@@ -0,0 +1,9 @@
+# coding: utf-8
+#
+
+from .base import *
+from .basic import *
+from .email import *
+from .ldap import *
+from .security import *
+from .terminal import *
diff --git a/apps/settings/forms/base.py b/apps/settings/forms/base.py
new file mode 100644
index 000000000..b7b32dea6
--- /dev/null
+++ b/apps/settings/forms/base.py
@@ -0,0 +1,57 @@
+# coding: utf-8
+#
+
+import json
+from django import forms
+from django.db import transaction
+from django.conf import settings
+
+from ..models import Setting
+from common.fields import FormEncryptMixin
+
+__all__ = ['BaseForm']
+
+
+class BaseForm(forms.Form):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ for name, field in self.fields.items():
+ value = getattr(settings, name, None)
+ if value is None: # and django_value is None:
+ continue
+
+ if value is not None:
+ if isinstance(value, dict):
+ value = json.dumps(value)
+ initial_value = value
+ else:
+ initial_value = ''
+ field.initial = initial_value
+
+ def save(self, category="default"):
+ if not self.is_bound:
+ raise ValueError("Form is not bound")
+
+ # db_settings = Setting.objects.all()
+ if not self.is_valid():
+ raise ValueError(self.errors)
+
+ with transaction.atomic():
+ for name, value in self.cleaned_data.items():
+ field = self.fields[name]
+ if isinstance(field.widget, forms.PasswordInput) and not value:
+ continue
+ # if value == getattr(settings, name):
+ # continue
+
+ encrypted = True if isinstance(field, FormEncryptMixin) else False
+ try:
+ setting = Setting.objects.get(name=name)
+ except Setting.DoesNotExist:
+ setting = Setting()
+ setting.name = name
+ setting.category = category
+ setting.encrypted = encrypted
+ setting.cleaned_value = value
+ setting.save()
+
diff --git a/apps/settings/forms/basic.py b/apps/settings/forms/basic.py
new file mode 100644
index 000000000..dd93c9537
--- /dev/null
+++ b/apps/settings/forms/basic.py
@@ -0,0 +1,24 @@
+# coding: utf-8
+#
+
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+from .base import BaseForm
+
+__all__ = ['BasicSettingForm']
+
+
+class BasicSettingForm(BaseForm):
+ SITE_URL = forms.URLField(
+ label=_("Current SITE URL"),
+ help_text="eg: http://jumpserver.abc.com:8080"
+ )
+ USER_GUIDE_URL = forms.URLField(
+ label=_("User Guide URL"), required=False,
+ help_text=_("User first login update profile done redirect to it")
+ )
+ EMAIL_SUBJECT_PREFIX = forms.CharField(
+ max_length=1024, label=_("Email Subject Prefix"),
+ help_text=_("Tips: Some word will be intercept by mail provider")
+ )
+
diff --git a/apps/settings/forms/email.py b/apps/settings/forms/email.py
new file mode 100644
index 000000000..6fa61148a
--- /dev/null
+++ b/apps/settings/forms/email.py
@@ -0,0 +1,65 @@
+# coding: utf-8
+#
+
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+
+from common.fields import FormEncryptCharField
+from .base import BaseForm
+
+__all__ = ['EmailSettingForm', 'EmailContentSettingForm']
+
+
+class EmailSettingForm(BaseForm):
+ EMAIL_HOST = forms.CharField(
+ max_length=1024, label=_("SMTP host"), initial='smtp.jumpserver.org'
+ )
+ EMAIL_PORT = forms.CharField(max_length=5, label=_("SMTP port"), initial=25)
+ EMAIL_HOST_USER = forms.CharField(
+ max_length=128, label=_("SMTP user"), initial='noreply@jumpserver.org'
+ )
+ EMAIL_HOST_PASSWORD = FormEncryptCharField(
+ max_length=1024, label=_("SMTP password"), widget=forms.PasswordInput,
+ required=False,
+ help_text=_("Tips: Some provider use token except password")
+ )
+ EMAIL_FROM = forms.CharField(
+ max_length=128, label=_("Send user"), initial='', required=False,
+ help_text=_(
+ "Tips: Send mail account, default SMTP account as the send account"
+ )
+ )
+ EMAIL_RECIPIENT = forms.CharField(
+ max_length=128, label=_("Test recipient"), initial='', required=False,
+ help_text=_("Tips: Used only as a test mail recipient")
+ )
+ EMAIL_USE_SSL = forms.BooleanField(
+ label=_("Use SSL"), initial=False, required=False,
+ help_text=_("If SMTP port is 465, may be select")
+ )
+ EMAIL_USE_TLS = forms.BooleanField(
+ label=_("Use TLS"), initial=False, required=False,
+ help_text=_("If SMTP port is 587, may be select")
+ )
+
+
+class EmailContentSettingForm(BaseForm):
+ EMAIL_CUSTOM_USER_CREATED_SUBJECT = forms.CharField(
+ max_length=1024, required=False, label=_("Create user email subject"),
+ help_text=_("Tips: When creating a user, send the subject of the email"
+ " (eg:Create account successfully)")
+ )
+ EMAIL_CUSTOM_USER_CREATED_HONORIFIC = forms.CharField(
+ max_length=1024, required=False, label=_("Create user honorific"),
+ help_text=_("Tips: When creating a user, send the honorific of the "
+ "email (eg:Hello)")
+ )
+ EMAIL_CUSTOM_USER_CREATED_BODY = forms.CharField(
+ max_length=4096, required=False, widget=forms.Textarea(),
+ label=_('Create user email content'),
+ help_text=_('Tips:When creating a user, send the content of the email')
+ )
+ EMAIL_CUSTOM_USER_CREATED_SIGNATURE = forms.CharField(
+ max_length=512, required=False, label=_("Signature"),
+ help_text=_("Tips: Email signature (eg:jumpserver)")
+ )
diff --git a/apps/settings/forms/ldap.py b/apps/settings/forms/ldap.py
new file mode 100644
index 000000000..c44d1c3e4
--- /dev/null
+++ b/apps/settings/forms/ldap.py
@@ -0,0 +1,46 @@
+# coding: utf-8
+#
+
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+
+from common.fields import FormDictField, FormEncryptCharField
+from .base import BaseForm
+
+
+__all__ = ['LDAPSettingForm']
+
+
+class LDAPSettingForm(BaseForm):
+ AUTH_LDAP_SERVER_URI = forms.CharField(
+ label=_("LDAP server"),
+ )
+ AUTH_LDAP_BIND_DN = forms.CharField(
+ required=False, label=_("Bind DN"),
+ )
+ AUTH_LDAP_BIND_PASSWORD = FormEncryptCharField(
+ label=_("Password"),
+ widget=forms.PasswordInput, required=False
+ )
+ AUTH_LDAP_SEARCH_OU = forms.CharField(
+ label=_("User OU"),
+ help_text=_("Use | split User OUs"),
+ required=False,
+ )
+ AUTH_LDAP_SEARCH_FILTER = forms.CharField(
+ label=_("User search filter"),
+ help_text=_("Choice may be (cn|uid|sAMAccountName)=%(user)s)")
+ )
+ AUTH_LDAP_USER_ATTR_MAP = FormDictField(
+ label=_("User attr map"),
+ help_text=_(
+ "User attr map present how to map LDAP user attr to jumpserver, "
+ "username,name,email is jumpserver attr"
+ ),
+ )
+ # AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU
+ # AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER
+ # AUTH_LDAP_START_TLS = forms.BooleanField(
+ # label=_("Use SSL"), required=False
+ # )
+ AUTH_LDAP = forms.BooleanField(label=_("Enable LDAP auth"), required=False)
diff --git a/apps/settings/forms/security.py b/apps/settings/forms/security.py
new file mode 100644
index 000000000..7b7cc247d
--- /dev/null
+++ b/apps/settings/forms/security.py
@@ -0,0 +1,93 @@
+# coding: utf-8
+#
+
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+
+from .base import BaseForm
+
+
+__all__ = ['SecuritySettingForm']
+
+
+class SecuritySettingForm(BaseForm):
+ # MFA global setting
+ SECURITY_MFA_AUTH = forms.BooleanField(
+ required=False, label=_("MFA Secondary certification"),
+ help_text=_(
+ 'After opening, the user login must use MFA secondary '
+ 'authentication (valid for all users, including administrators)'
+ )
+ )
+ # Execute commands for user
+ SECURITY_COMMAND_EXECUTION = forms.BooleanField(
+ required=False, label=_("Batch execute commands"),
+ help_text=_("Allow user batch execute commands")
+ )
+ SECURITY_SERVICE_ACCOUNT_REGISTRATION = forms.BooleanField(
+ required=False, label=_("Service account registration"),
+ help_text=_("Allow using bootstrap token register service account, "
+ "when terminal setup, can disable it")
+ )
+ # limit login count
+ SECURITY_LOGIN_LIMIT_COUNT = forms.IntegerField(
+ min_value=3, max_value=99999,
+ label=_("Limit the number of login failures")
+ )
+ # limit login time
+ SECURITY_LOGIN_LIMIT_TIME = forms.IntegerField(
+ min_value=5, max_value=99999, label=_("No logon interval"),
+ help_text=_(
+ "Tip: (unit/minute) if the user has failed to log in for a limited "
+ "number of times, no login is allowed during this time interval."
+ )
+ )
+ # ssh max idle time
+ SECURITY_MAX_IDLE_TIME = forms.IntegerField(
+ min_value=1, max_value=99999, required=False,
+ label=_("Connection max idle time"),
+ help_text=_(
+ 'If idle time more than it, disconnect connection '
+ 'Unit: minute'
+ ),
+ )
+ # password expiration time
+ SECURITY_PASSWORD_EXPIRATION_TIME = forms.IntegerField(
+ min_value=1, max_value=99999, label=_("Password expiration time"),
+ help_text=_(
+ "Tip: (unit: day) "
+ "If the user does not update the password during the time, "
+ "the user password will expire failure;"
+ "The password expiration reminder mail will be automatic sent to the user "
+ "by system within 5 days (daily) before the password expires"
+ )
+ )
+ # min length
+ SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField(
+ min_value=6, max_value=30, label=_("Password minimum length"),
+ )
+ # upper case
+ SECURITY_PASSWORD_UPPER_CASE = forms.BooleanField(
+ required=False, label=_("Must contain capital letters"),
+ help_text=_(
+ 'After opening, the user password changes '
+ 'and resets must contain uppercase letters')
+ )
+ # lower case
+ SECURITY_PASSWORD_LOWER_CASE = forms.BooleanField(
+ required=False, label=_("Must contain lowercase letters"),
+ help_text=_('After opening, the user password changes '
+ 'and resets must contain lowercase letters')
+ )
+ # number
+ SECURITY_PASSWORD_NUMBER = forms.BooleanField(
+ required=False, label=_("Must contain numeric characters"),
+ help_text=_('After opening, the user password changes '
+ 'and resets must contain numeric characters')
+ )
+ # special char
+ SECURITY_PASSWORD_SPECIAL_CHAR = forms.BooleanField(
+ required=False, label=_("Must contain special characters"),
+ help_text=_('After opening, the user password changes '
+ 'and resets must contain special characters')
+ )
diff --git a/apps/settings/forms/terminal.py b/apps/settings/forms/terminal.py
new file mode 100644
index 000000000..d879e88d2
--- /dev/null
+++ b/apps/settings/forms/terminal.py
@@ -0,0 +1,50 @@
+# coding: utf-8
+#
+
+
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+
+from .base import BaseForm
+
+__all__ = ['TerminalSettingForm']
+
+
+class TerminalSettingForm(BaseForm):
+ SORT_BY_CHOICES = (
+ ('hostname', _('Hostname')),
+ ('ip', _('IP')),
+ )
+ PAGE_SIZE_CHOICES = (
+ ('all', _('All')),
+ ('auto', _('Auto')),
+ (10, 10),
+ (15, 15),
+ (25, 25),
+ (50, 50),
+ )
+ TERMINAL_PASSWORD_AUTH = forms.BooleanField(
+ required=False, label=_("Password auth")
+ )
+ TERMINAL_PUBLIC_KEY_AUTH = forms.BooleanField(
+ required=False, label=_("Public key auth")
+ )
+ TERMINAL_HEARTBEAT_INTERVAL = forms.IntegerField(
+ min_value=5, max_value=99999, label=_("Heartbeat interval"),
+ help_text=_("Units: seconds")
+ )
+ TERMINAL_ASSET_LIST_SORT_BY = forms.ChoiceField(
+ choices=SORT_BY_CHOICES, label=_("List sort by")
+ )
+ TERMINAL_ASSET_LIST_PAGE_SIZE = forms.ChoiceField(
+ choices=PAGE_SIZE_CHOICES, label=_("List page size"),
+ )
+ TERMINAL_SESSION_KEEP_DURATION = forms.IntegerField(
+ min_value=1, max_value=99999, label=_("Session keep duration"),
+ help_text=_("Units: days, Session, record, command will be delete "
+ "if more than duration, only in database")
+ )
+ TERMINAL_TELNET_REGEX = forms.CharField(
+ required=False, label=_("Telnet login regex"),
+ help_text=_("ex: Last\s*login|success|成功")
+ )
diff --git a/apps/settings/models.py b/apps/settings/models.py
index 524fa9349..75b56bb54 100644
--- a/apps/settings/models.py
+++ b/apps/settings/models.py
@@ -1,14 +1,11 @@
import json
from django.db import models
-from django.core.cache import cache
from django.db.utils import ProgrammingError, OperationalError
from django.utils.translation import ugettext_lazy as _
-from django.conf import settings
+from django.core.cache import cache
-from common.utils import get_signer
-
-signer = get_signer()
+from common.utils import signer
class SettingQuerySet(models.QuerySet):
@@ -34,12 +31,28 @@ class Setting(models.Model):
comment = models.TextField(verbose_name=_("Comment"))
objects = SettingManager()
+ cache_key_prefix = '_SETTING_'
def __str__(self):
return self.name
- def __getattr__(self, item):
- return cache.get(item)
+ @classmethod
+ def get(cls, item):
+ cached = cls.get_from_cache(item)
+ if cached is not None:
+ return cached
+ instances = cls.objects.filter(name=item)
+ if len(instances) == 1:
+ s = instances[0]
+ s.refresh_setting()
+ return s.cleaned_value
+ return None
+
+ @classmethod
+ def get_from_cache(cls, item):
+ key = cls.cache_key_prefix + item
+ cached = cache.get(key)
+ return cached
@property
def cleaned_value(self):
@@ -64,44 +77,6 @@ class Setting(models.Model):
except json.JSONDecodeError as e:
raise ValueError("Json dump error: {}".format(str(e)))
- @classmethod
- def save_storage(cls, name, data):
- """
- :param name: TERMINAL_REPLAY_STORAGE or TERMINAL_COMMAND_STORAGE
- :param data: {}
- :return: Setting object
- """
- obj = cls.objects.filter(name=name).first()
- if not obj:
- obj = cls()
- obj.name = name
- obj.encrypted = True
- obj.cleaned_value = data
- else:
- value = obj.cleaned_value
- if value is None:
- value = {}
- value.update(data)
- obj.cleaned_value = value
- obj.save()
- return obj
-
- @classmethod
- def delete_storage(cls, name, storage_name):
- """
- :param name: TERMINAL_REPLAY_STORAGE or TERMINAL_COMMAND_STORAGE
- :param storage_name: ""
- :return: bool
- """
- obj = cls.objects.filter(name=name).first()
- if not obj:
- return False
- value = obj.cleaned_value
- value.pop(storage_name, '')
- obj.cleaned_value = value
- obj.save()
- return True
-
@classmethod
def refresh_all_settings(cls):
try:
@@ -112,16 +87,8 @@ class Setting(models.Model):
pass
def refresh_setting(self):
- setattr(settings, self.name, self.cleaned_value)
- if self.name == "AUTH_LDAP":
- if self.cleaned_value and settings.AUTH_LDAP_BACKEND not in settings.AUTHENTICATION_BACKENDS:
- old_setting = settings.AUTHENTICATION_BACKENDS
- old_setting.insert(0, settings.AUTH_LDAP_BACKEND)
- settings.AUTHENTICATION_BACKENDS = old_setting
- elif not self.cleaned_value and settings.AUTH_LDAP_BACKEND in settings.AUTHENTICATION_BACKENDS:
- old_setting = settings.AUTHENTICATION_BACKENDS
- old_setting.remove(settings.AUTH_LDAP_BACKEND)
- settings.AUTHENTICATION_BACKENDS = old_setting
+ key = self.cache_key_prefix + self.name
+ cache.set(key, self.cleaned_value, None)
class Meta:
db_table = "settings_setting"
diff --git a/apps/settings/serializers/__init__.py b/apps/settings/serializers/__init__.py
new file mode 100644
index 000000000..045763364
--- /dev/null
+++ b/apps/settings/serializers/__init__.py
@@ -0,0 +1,6 @@
+# coding: utf-8
+#
+
+from .email import *
+from .ldap import *
+from .public import *
diff --git a/apps/settings/serializers/email.py b/apps/settings/serializers/email.py
new file mode 100644
index 000000000..6d033f6aa
--- /dev/null
+++ b/apps/settings/serializers/email.py
@@ -0,0 +1,17 @@
+# coding: utf-8
+#
+
+from rest_framework import serializers
+
+__all__ = ['MailTestSerializer']
+
+
+class MailTestSerializer(serializers.Serializer):
+ EMAIL_HOST = serializers.CharField(max_length=1024, required=True)
+ EMAIL_PORT = serializers.IntegerField(default=25)
+ EMAIL_HOST_USER = serializers.CharField(max_length=1024)
+ EMAIL_HOST_PASSWORD = serializers.CharField(required=False, allow_blank=True)
+ EMAIL_FROM = serializers.CharField(required=False, allow_blank=True)
+ EMAIL_RECIPIENT = serializers.CharField(required=False, allow_blank=True)
+ EMAIL_USE_SSL = serializers.BooleanField(default=False)
+ EMAIL_USE_TLS = serializers.BooleanField(default=False)
diff --git a/apps/settings/serializers.py b/apps/settings/serializers/ldap.py
similarity index 54%
rename from apps/settings/serializers.py
rename to apps/settings/serializers/ldap.py
index 1717634f4..4009c0705 100644
--- a/apps/settings/serializers.py
+++ b/apps/settings/serializers/ldap.py
@@ -1,15 +1,9 @@
+# coding: utf-8
+#
+
from rest_framework import serializers
-
-class MailTestSerializer(serializers.Serializer):
- EMAIL_HOST = serializers.CharField(max_length=1024, required=True)
- EMAIL_PORT = serializers.IntegerField(default=25)
- EMAIL_HOST_USER = serializers.CharField(max_length=1024)
- EMAIL_HOST_PASSWORD = serializers.CharField(required=False, allow_blank=True)
- EMAIL_FROM = serializers.CharField(required=False, allow_blank=True)
- EMAIL_RECIPIENT = serializers.CharField(required=False, allow_blank=True)
- EMAIL_USE_SSL = serializers.BooleanField(default=False)
- EMAIL_USE_TLS = serializers.BooleanField(default=False)
+__all__ = ['LDAPTestSerializer', 'LDAPUserSerializer']
class LDAPTestSerializer(serializers.Serializer):
@@ -29,6 +23,3 @@ class LDAPUserSerializer(serializers.Serializer):
email = serializers.CharField()
existing = serializers.BooleanField(read_only=True)
-
-class PublicSettingSerializer(serializers.Serializer):
- data = serializers.DictField(read_only=True)
diff --git a/apps/settings/serializers/public.py b/apps/settings/serializers/public.py
new file mode 100644
index 000000000..52e39a954
--- /dev/null
+++ b/apps/settings/serializers/public.py
@@ -0,0 +1,10 @@
+# coding: utf-8
+#
+
+from rest_framework import serializers
+
+__all__ = ['PublicSettingSerializer']
+
+
+class PublicSettingSerializer(serializers.Serializer):
+ data = serializers.DictField(read_only=True)
diff --git a/apps/settings/signals_handler.py b/apps/settings/signals_handler.py
index c131cc214..026d60a78 100644
--- a/apps/settings/signals_handler.py
+++ b/apps/settings/signals_handler.py
@@ -4,9 +4,6 @@ import json
from django.dispatch import receiver
from django.db.models.signals import post_save, pre_save
-from django.conf import LazySettings, empty, global_settings
-from django.db.utils import ProgrammingError, OperationalError
-from django.core.cache import cache
from jumpserver.utils import current_request
from common.utils import get_logger, ssh_key_gen
@@ -23,56 +20,9 @@ def refresh_settings_on_changed(sender, instance=None, **kwargs):
@receiver(django_ready)
-def monkey_patch_settings(sender, **kwargs):
- logger.debug("Monkey patch settings")
- cache_key_prefix = '_SETTING_'
- custom_need_cache_settings = [
- 'AUTHENTICATION_BACKENDS', 'TERMINAL_HOST_KEY',
- ]
- custom_no_cache_settings = [
- 'BASE_DIR', 'VERSION', 'AUTH_OPENID',
- ]
- django_settings = dir(global_settings)
- uncached_settings = [i for i in django_settings if i.isupper()]
- uncached_settings = [i for i in uncached_settings if not i.startswith('EMAIL')]
- uncached_settings = [i for i in uncached_settings if not i.startswith('SESSION_REDIS')]
- uncached_settings = [i for i in uncached_settings if i not in custom_need_cache_settings]
- uncached_settings.extend(custom_no_cache_settings)
-
- def monkey_patch_getattr(self, name):
- if name not in uncached_settings:
- key = cache_key_prefix + name
- cached = cache.get(key)
- if cached is not None:
- return cached
- if self._wrapped is empty:
- self._setup(name)
- val = getattr(self._wrapped, name)
- return val
-
- def monkey_patch_setattr(self, name, value):
- key = cache_key_prefix + name
- cache.set(key, value, None)
- if name == '_wrapped':
- self.__dict__.clear()
- else:
- self.__dict__.pop(name, None)
- super(LazySettings, self).__setattr__(name, value)
-
- def monkey_patch_delattr(self, name):
- super(LazySettings, self).__delattr__(name)
- self.__dict__.pop(name, None)
- key = cache_key_prefix + name
- cache.delete(key)
-
- try:
- cache.delete_pattern(cache_key_prefix+'*')
- LazySettings.__getattr__ = monkey_patch_getattr
- LazySettings.__setattr__ = monkey_patch_setattr
- LazySettings.__delattr__ = monkey_patch_delattr
- Setting.refresh_all_settings()
- except (ProgrammingError, OperationalError):
- pass
+def on_django_ready_add_db_config(sender, **kwargs):
+ from django.conf import settings
+ settings.DYNAMIC.db_setting = Setting
@receiver(django_ready)
diff --git a/apps/settings/templates/settings/_setting_tabs.html b/apps/settings/templates/settings/_setting_tabs.html
new file mode 100644
index 000000000..b012a6669
--- /dev/null
+++ b/apps/settings/templates/settings/_setting_tabs.html
@@ -0,0 +1,37 @@
+{% load i18n %}
+
+
+
diff --git a/apps/settings/templates/settings/basic_setting.html b/apps/settings/templates/settings/basic_setting.html
index 4c26e8bb3..ac8cabb8b 100644
--- a/apps/settings/templates/settings/basic_setting.html
+++ b/apps/settings/templates/settings/basic_setting.html
@@ -10,26 +10,7 @@
-
+ {% include 'settings/_setting_tabs.html' %}
diff --git a/apps/settings/templates/settings/command_storage_create.html b/apps/settings/templates/settings/command_storage_create.html
deleted file mode 100644
index 9c60e2d85..000000000
--- a/apps/settings/templates/settings/command_storage_create.html
+++ /dev/null
@@ -1,175 +0,0 @@
-{#{% extends 'base.html' %}#}
-{% extends '_base_create_update.html' %}
-{% load static %}
-{% load bootstrap3 %}
-{% load i18n %}
-{% load common_tags %}
-
-{% block content %}
-
-{% endblock %}
-
-{% block custom_foot_js %}
-
-{% endblock %}
diff --git a/apps/settings/templates/settings/email_content_setting.html b/apps/settings/templates/settings/email_content_setting.html
index 16cac426e..c2c5b2720 100644
--- a/apps/settings/templates/settings/email_content_setting.html
+++ b/apps/settings/templates/settings/email_content_setting.html
@@ -10,53 +10,33 @@
-
+ {% include 'settings/_setting_tabs.html' %}
-
diff --git a/apps/settings/templates/settings/email_setting.html b/apps/settings/templates/settings/email_setting.html
index 40ce9f4cc..d62a921cd 100644
--- a/apps/settings/templates/settings/email_setting.html
+++ b/apps/settings/templates/settings/email_setting.html
@@ -10,26 +10,7 @@
-
+ {% include 'settings/_setting_tabs.html' %}
diff --git a/apps/settings/templates/settings/ldap_setting.html b/apps/settings/templates/settings/ldap_setting.html
index 5da66405d..943a69322 100644
--- a/apps/settings/templates/settings/ldap_setting.html
+++ b/apps/settings/templates/settings/ldap_setting.html
@@ -10,26 +10,7 @@
-
+ {% include 'settings/_setting_tabs.html' %}
diff --git a/apps/settings/templates/settings/replay_storage_create.html b/apps/settings/templates/settings/replay_storage_create.html
deleted file mode 100644
index 6521a730a..000000000
--- a/apps/settings/templates/settings/replay_storage_create.html
+++ /dev/null
@@ -1,277 +0,0 @@
-{#{% extends 'base.html' %}#}
-{% extends '_base_create_update.html' %}
-{% load static %}
-{% load bootstrap3 %}
-{% load i18n %}
-{% load common_tags %}
-
-{% block content %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-{% endblock %}
-
-{% block custom_foot_js %}
-
-{% endblock %}
diff --git a/apps/settings/templates/settings/security_setting.html b/apps/settings/templates/settings/security_setting.html
index 48206676d..663632a72 100644
--- a/apps/settings/templates/settings/security_setting.html
+++ b/apps/settings/templates/settings/security_setting.html
@@ -10,26 +10,7 @@
-
+ {% include 'settings/_setting_tabs.html' %}
@@ -44,7 +25,7 @@
{% trans "Security setting" %}
{% for field in form %}
- {% if forloop.counter == 6 %}
+ {% if forloop.counter == 8 %}
{% trans "Password check rule" %}
{% endif %}
diff --git a/apps/settings/templates/settings/terminal_setting.html b/apps/settings/templates/settings/terminal_setting.html
index 3a9a4973d..a0b35aabb 100644
--- a/apps/settings/templates/settings/terminal_setting.html
+++ b/apps/settings/templates/settings/terminal_setting.html
@@ -3,6 +3,11 @@
{% load bootstrap3 %}
{% load i18n %}
{% load common_tags %}
+{% block help_message %}
+ {% trans "Command and Replay storage configuration migrated to" %}
+ {% trans "Sessions -> Terminal -> Storage configuration" %}
+
{% trans 'Here' %}
+{% endblock %}
{% block content %}
@@ -10,30 +15,7 @@
-
+ {% include 'settings/_setting_tabs.html' %}
@@ -65,7 +47,7 @@
{% endif %}
{% endfor %}
-
+
-
-
-
-
{% trans "Command storage" %}
-
-
-
- {% trans 'Name' %}
- {% trans 'Type' %}
- {% trans 'Action' %}
-
-
-
- {% for name, setting in command_storage.items %}
-
- {{ name }}
- {{ setting.TYPE }}
- {% trans 'Delete' %}
-
- {% endfor %}
-
-
-
{% trans 'Add' %}
-
-
-
{% trans "Replay storage" %}
-
-
-
- {% trans 'Name' %}
- {% trans 'Type' %}
- {% trans 'Action' %}
-
-
-
- {% for name, setting in replay_storage.items %}
-
- {{ name }}
- {{ setting.TYPE }}
- {% trans 'Delete' %}
-
- {% endfor %}
-
-
-
{% trans 'Add' %}
-
-
@@ -131,60 +66,7 @@
{% endblock %}
{% block custom_foot_js %}
{% endblock %}
diff --git a/apps/settings/urls/api_urls.py b/apps/settings/urls/api_urls.py
index 544de3941..abee1c0d0 100644
--- a/apps/settings/urls/api_urls.py
+++ b/apps/settings/urls/api_urls.py
@@ -12,9 +12,6 @@ urlpatterns = [
path('ldap/users/', api.LDAPUserListApi.as_view(), name='ldap-user-list'),
path('ldap/users/import/', api.LDAPUserImportAPI.as_view(), name='ldap-user-import'),
path('ldap/cache/refresh/', api.LDAPCacheRefreshAPI.as_view(), name='ldap-cache-refresh'),
- path('terminal/replay-storage/create/', api.ReplayStorageCreateAPI.as_view(), name='replay-storage-create'),
- path('terminal/replay-storage/delete/', api.ReplayStorageDeleteAPI.as_view(), name='replay-storage-delete'),
- path('terminal/command-storage/create/', api.CommandStorageCreateAPI.as_view(), name='command-storage-create'),
- path('terminal/command-storage/delete/', api.CommandStorageDeleteAPI.as_view(), name='command-storage-delete'),
+
path('public/', api.PublicSettingApi.as_view(), name='public-setting'),
]
diff --git a/apps/settings/urls/view_urls.py b/apps/settings/urls/view_urls.py
index eac18a432..6a1c5baaf 100644
--- a/apps/settings/urls/view_urls.py
+++ b/apps/settings/urls/view_urls.py
@@ -12,7 +12,5 @@ urlpatterns = [
url(r'^email-content/$', views.EmailContentSettingView.as_view(), name='email-content-setting'),
url(r'^ldap/$', views.LDAPSettingView.as_view(), name='ldap-setting'),
url(r'^terminal/$', views.TerminalSettingView.as_view(), name='terminal-setting'),
- url(r'^terminal/replay-storage/create$', views.ReplayStorageCreateView.as_view(), name='replay-storage-create'),
- url(r'^terminal/command-storage/create$', views.CommandStorageCreateView.as_view(), name='command-storage-create'),
url(r'^security/$', views.SecuritySettingView.as_view(), name='security-setting'),
]
diff --git a/apps/settings/views.py b/apps/settings/views.py
index 2442f074e..6ebbeef97 100644
--- a/apps/settings/views.py
+++ b/apps/settings/views.py
@@ -4,7 +4,6 @@ from django.contrib import messages
from django.utils.translation import ugettext as _
from common.permissions import PermissionsMixin, IsSuperUser
-from common import utils
from .utils import LDAPSyncUtil
from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm, \
TerminalSettingForm, SecuritySettingForm, EmailContentSettingForm
@@ -98,8 +97,9 @@ class TerminalSettingView(PermissionsMixin, TemplateView):
permission_classes = [IsSuperUser]
def get_context_data(self, **kwargs):
- command_storage = utils.get_command_storage_setting()
- replay_storage = utils.get_replay_storage_setting()
+ from terminal.models import CommandStorage, ReplayStorage
+ command_storage = CommandStorage.objects.all()
+ replay_storage = ReplayStorage.objects.all()
context = {
'app': _('Settings'),
@@ -124,32 +124,6 @@ class TerminalSettingView(PermissionsMixin, TemplateView):
return render(request, self.template_name, context)
-class ReplayStorageCreateView(PermissionsMixin, TemplateView):
- template_name = 'settings/replay_storage_create.html'
- permission_classes = [IsSuperUser]
-
- def get_context_data(self, **kwargs):
- context = {
- 'app': _('Settings'),
- 'action': _('Create replay storage')
- }
- kwargs.update(context)
- return super().get_context_data(**kwargs)
-
-
-class CommandStorageCreateView(PermissionsMixin, TemplateView):
- template_name = 'settings/command_storage_create.html'
- permission_classes = [IsSuperUser]
-
- def get_context_data(self, **kwargs):
- context = {
- 'app': _('Settings'),
- 'action': _('Create command storage')
- }
- kwargs.update(context)
- return super().get_context_data(**kwargs)
-
-
class SecuritySettingView(PermissionsMixin, TemplateView):
form_class = SecuritySettingForm
template_name = "settings/security_setting.html"
diff --git a/apps/static/css/plugins/ladda/ladda-themeless.min.css b/apps/static/css/plugins/ladda/ladda-themeless.min.css
new file mode 100755
index 000000000..6dee68811
--- /dev/null
+++ b/apps/static/css/plugins/ladda/ladda-themeless.min.css
@@ -0,0 +1,7 @@
+/*!
+ * Ladda
+ * http://lab.hakim.se/ladda
+ * MIT licensed
+ *
+ * Copyright (C) 2015 Hakim El Hattab, http://hakim.se
+ */.ladda-button{position:relative}.ladda-button .ladda-spinner{position:absolute;z-index:2;display:inline-block;width:32px;height:32px;top:50%;margin-top:0;opacity:0;pointer-events:none}.ladda-button .ladda-label{position:relative;z-index:3}.ladda-button .ladda-progress{position:absolute;width:0;height:100%;left:0;top:0;background:rgba(0,0,0,0.2);visibility:hidden;opacity:0;-webkit-transition:0.1s linear all !important;-moz-transition:0.1s linear all !important;-ms-transition:0.1s linear all !important;-o-transition:0.1s linear all !important;transition:0.1s linear all !important}.ladda-button[data-loading] .ladda-progress{opacity:1;visibility:visible}.ladda-button,.ladda-button .ladda-spinner,.ladda-button .ladda-label{-webkit-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;-moz-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;-ms-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;-o-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important}.ladda-button[data-style=zoom-in],.ladda-button[data-style=zoom-in] .ladda-spinner,.ladda-button[data-style=zoom-in] .ladda-label,.ladda-button[data-style=zoom-out],.ladda-button[data-style=zoom-out] .ladda-spinner,.ladda-button[data-style=zoom-out] .ladda-label{-webkit-transition:0.3s ease all !important;-moz-transition:0.3s ease all !important;-ms-transition:0.3s ease all !important;-o-transition:0.3s ease all !important;transition:0.3s ease all !important}.ladda-button[data-style=expand-right] .ladda-spinner{right:-6px}.ladda-button[data-style=expand-right][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-right][data-size="xs"] .ladda-spinner{right:-12px}.ladda-button[data-style=expand-right][data-loading]{padding-right:56px}.ladda-button[data-style=expand-right][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=expand-right][data-loading][data-size="s"],.ladda-button[data-style=expand-right][data-loading][data-size="xs"]{padding-right:40px}.ladda-button[data-style=expand-left] .ladda-spinner{left:26px}.ladda-button[data-style=expand-left][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-left][data-size="xs"] .ladda-spinner{left:4px}.ladda-button[data-style=expand-left][data-loading]{padding-left:56px}.ladda-button[data-style=expand-left][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=expand-left][data-loading][data-size="s"],.ladda-button[data-style=expand-left][data-loading][data-size="xs"]{padding-left:40px}.ladda-button[data-style=expand-up]{overflow:hidden}.ladda-button[data-style=expand-up] .ladda-spinner{top:-32px;left:50%;margin-left:0}.ladda-button[data-style=expand-up][data-loading]{padding-top:54px}.ladda-button[data-style=expand-up][data-loading] .ladda-spinner{opacity:1;top:26px;margin-top:0}.ladda-button[data-style=expand-up][data-loading][data-size="s"],.ladda-button[data-style=expand-up][data-loading][data-size="xs"]{padding-top:32px}.ladda-button[data-style=expand-up][data-loading][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-up][data-loading][data-size="xs"] .ladda-spinner{top:4px}.ladda-button[data-style=expand-down]{overflow:hidden}.ladda-button[data-style=expand-down] .ladda-spinner{top:62px;left:50%;margin-left:0}.ladda-button[data-style=expand-down][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-down][data-size="xs"] .ladda-spinner{top:40px}.ladda-button[data-style=expand-down][data-loading]{padding-bottom:54px}.ladda-button[data-style=expand-down][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=expand-down][data-loading][data-size="s"],.ladda-button[data-style=expand-down][data-loading][data-size="xs"]{padding-bottom:32px}.ladda-button[data-style=slide-left]{overflow:hidden}.ladda-button[data-style=slide-left] .ladda-label{position:relative}.ladda-button[data-style=slide-left] .ladda-spinner{left:100%;margin-left:0}.ladda-button[data-style=slide-left][data-loading] .ladda-label{opacity:0;left:-100%}.ladda-button[data-style=slide-left][data-loading] .ladda-spinner{opacity:1;left:50%}.ladda-button[data-style=slide-right]{overflow:hidden}.ladda-button[data-style=slide-right] .ladda-label{position:relative}.ladda-button[data-style=slide-right] .ladda-spinner{right:100%;margin-left:0;left:16px}.ladda-button[data-style=slide-right][data-loading] .ladda-label{opacity:0;left:100%}.ladda-button[data-style=slide-right][data-loading] .ladda-spinner{opacity:1;left:50%}.ladda-button[data-style=slide-up]{overflow:hidden}.ladda-button[data-style=slide-up] .ladda-label{position:relative}.ladda-button[data-style=slide-up] .ladda-spinner{left:50%;margin-left:0;margin-top:1em}.ladda-button[data-style=slide-up][data-loading] .ladda-label{opacity:0;top:-1em}.ladda-button[data-style=slide-up][data-loading] .ladda-spinner{opacity:1;margin-top:0}.ladda-button[data-style=slide-down]{overflow:hidden}.ladda-button[data-style=slide-down] .ladda-label{position:relative}.ladda-button[data-style=slide-down] .ladda-spinner{left:50%;margin-left:0;margin-top:-2em}.ladda-button[data-style=slide-down][data-loading] .ladda-label{opacity:0;top:1em}.ladda-button[data-style=slide-down][data-loading] .ladda-spinner{opacity:1;margin-top:0}.ladda-button[data-style=zoom-out]{overflow:hidden}.ladda-button[data-style=zoom-out] .ladda-spinner{left:50%;margin-left:32px;-webkit-transform:scale(2.5);-moz-transform:scale(2.5);-ms-transform:scale(2.5);-o-transform:scale(2.5);transform:scale(2.5)}.ladda-button[data-style=zoom-out] .ladda-label{position:relative;display:inline-block}.ladda-button[data-style=zoom-out][data-loading] .ladda-label{opacity:0;-webkit-transform:scale(0.5);-moz-transform:scale(0.5);-ms-transform:scale(0.5);-o-transform:scale(0.5);transform:scale(0.5)}.ladda-button[data-style=zoom-out][data-loading] .ladda-spinner{opacity:1;margin-left:0;-webkit-transform:none;-moz-transform:none;-ms-transform:none;-o-transform:none;transform:none}.ladda-button[data-style=zoom-in]{overflow:hidden}.ladda-button[data-style=zoom-in] .ladda-spinner{left:50%;margin-left:-16px;-webkit-transform:scale(0.2);-moz-transform:scale(0.2);-ms-transform:scale(0.2);-o-transform:scale(0.2);transform:scale(0.2)}.ladda-button[data-style=zoom-in] .ladda-label{position:relative;display:inline-block}.ladda-button[data-style=zoom-in][data-loading] .ladda-label{opacity:0;-webkit-transform:scale(2.2);-moz-transform:scale(2.2);-ms-transform:scale(2.2);-o-transform:scale(2.2);transform:scale(2.2)}.ladda-button[data-style=zoom-in][data-loading] .ladda-spinner{opacity:1;margin-left:0;-webkit-transform:none;-moz-transform:none;-ms-transform:none;-o-transform:none;transform:none}.ladda-button[data-style=contract]{overflow:hidden;width:100px}.ladda-button[data-style=contract] .ladda-spinner{left:50%;margin-left:0}.ladda-button[data-style=contract][data-loading]{border-radius:50%;width:52px}.ladda-button[data-style=contract][data-loading] .ladda-label{opacity:0}.ladda-button[data-style=contract][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=contract-overlay]{overflow:hidden;width:100px;box-shadow:0px 0px 0px 2000px transparent}.ladda-button[data-style=contract-overlay] .ladda-spinner{left:50%;margin-left:0}.ladda-button[data-style=contract-overlay][data-loading]{border-radius:50%;width:52px;box-shadow:0px 0px 0px 2000px rgba(0,0,0,0.8)}.ladda-button[data-style=contract-overlay][data-loading] .ladda-label{opacity:0}.ladda-button[data-style=contract-overlay][data-loading] .ladda-spinner{opacity:1}
diff --git a/apps/static/css/plugins/ladda/ladda.min.css b/apps/static/css/plugins/ladda/ladda.min.css
new file mode 100755
index 000000000..7a42a10f0
--- /dev/null
+++ b/apps/static/css/plugins/ladda/ladda.min.css
@@ -0,0 +1,9 @@
+/*!
+ * Ladda including the default theme.
+ *//*!
+ * Ladda
+ * http://lab.hakim.se/ladda
+ * MIT licensed
+ *
+ * Copyright (C) 2015 Hakim El Hattab, http://hakim.se
+ */.ladda-button{position:relative}.ladda-button .ladda-spinner{position:absolute;z-index:2;display:inline-block;width:32px;height:32px;top:50%;margin-top:0;opacity:0;pointer-events:none}.ladda-button .ladda-label{position:relative;z-index:3}.ladda-button .ladda-progress{position:absolute;width:0;height:100%;left:0;top:0;background:rgba(0,0,0,0.2);visibility:hidden;opacity:0;-webkit-transition:0.1s linear all !important;-moz-transition:0.1s linear all !important;-ms-transition:0.1s linear all !important;-o-transition:0.1s linear all !important;transition:0.1s linear all !important}.ladda-button[data-loading] .ladda-progress{opacity:1;visibility:visible}.ladda-button,.ladda-button .ladda-spinner,.ladda-button .ladda-label{-webkit-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;-moz-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;-ms-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;-o-transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important;transition:0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) all !important}.ladda-button[data-style=zoom-in],.ladda-button[data-style=zoom-in] .ladda-spinner,.ladda-button[data-style=zoom-in] .ladda-label,.ladda-button[data-style=zoom-out],.ladda-button[data-style=zoom-out] .ladda-spinner,.ladda-button[data-style=zoom-out] .ladda-label{-webkit-transition:0.3s ease all !important;-moz-transition:0.3s ease all !important;-ms-transition:0.3s ease all !important;-o-transition:0.3s ease all !important;transition:0.3s ease all !important}.ladda-button[data-style=expand-right] .ladda-spinner{right:-6px}.ladda-button[data-style=expand-right][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-right][data-size="xs"] .ladda-spinner{right:-12px}.ladda-button[data-style=expand-right][data-loading]{padding-right:56px}.ladda-button[data-style=expand-right][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=expand-right][data-loading][data-size="s"],.ladda-button[data-style=expand-right][data-loading][data-size="xs"]{padding-right:40px}.ladda-button[data-style=expand-left] .ladda-spinner{left:26px}.ladda-button[data-style=expand-left][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-left][data-size="xs"] .ladda-spinner{left:4px}.ladda-button[data-style=expand-left][data-loading]{padding-left:56px}.ladda-button[data-style=expand-left][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=expand-left][data-loading][data-size="s"],.ladda-button[data-style=expand-left][data-loading][data-size="xs"]{padding-left:40px}.ladda-button[data-style=expand-up]{overflow:hidden}.ladda-button[data-style=expand-up] .ladda-spinner{top:-32px;left:50%;margin-left:0}.ladda-button[data-style=expand-up][data-loading]{padding-top:54px}.ladda-button[data-style=expand-up][data-loading] .ladda-spinner{opacity:1;top:26px;margin-top:0}.ladda-button[data-style=expand-up][data-loading][data-size="s"],.ladda-button[data-style=expand-up][data-loading][data-size="xs"]{padding-top:32px}.ladda-button[data-style=expand-up][data-loading][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-up][data-loading][data-size="xs"] .ladda-spinner{top:4px}.ladda-button[data-style=expand-down]{overflow:hidden}.ladda-button[data-style=expand-down] .ladda-spinner{top:62px;left:50%;margin-left:0}.ladda-button[data-style=expand-down][data-size="s"] .ladda-spinner,.ladda-button[data-style=expand-down][data-size="xs"] .ladda-spinner{top:40px}.ladda-button[data-style=expand-down][data-loading]{padding-bottom:54px}.ladda-button[data-style=expand-down][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=expand-down][data-loading][data-size="s"],.ladda-button[data-style=expand-down][data-loading][data-size="xs"]{padding-bottom:32px}.ladda-button[data-style=slide-left]{overflow:hidden}.ladda-button[data-style=slide-left] .ladda-label{position:relative}.ladda-button[data-style=slide-left] .ladda-spinner{left:100%;margin-left:0}.ladda-button[data-style=slide-left][data-loading] .ladda-label{opacity:0;left:-100%}.ladda-button[data-style=slide-left][data-loading] .ladda-spinner{opacity:1;left:50%}.ladda-button[data-style=slide-right]{overflow:hidden}.ladda-button[data-style=slide-right] .ladda-label{position:relative}.ladda-button[data-style=slide-right] .ladda-spinner{right:100%;margin-left:0;left:16px}.ladda-button[data-style=slide-right][data-loading] .ladda-label{opacity:0;left:100%}.ladda-button[data-style=slide-right][data-loading] .ladda-spinner{opacity:1;left:50%}.ladda-button[data-style=slide-up]{overflow:hidden}.ladda-button[data-style=slide-up] .ladda-label{position:relative}.ladda-button[data-style=slide-up] .ladda-spinner{left:50%;margin-left:0;margin-top:1em}.ladda-button[data-style=slide-up][data-loading] .ladda-label{opacity:0;top:-1em}.ladda-button[data-style=slide-up][data-loading] .ladda-spinner{opacity:1;margin-top:0}.ladda-button[data-style=slide-down]{overflow:hidden}.ladda-button[data-style=slide-down] .ladda-label{position:relative}.ladda-button[data-style=slide-down] .ladda-spinner{left:50%;margin-left:0;margin-top:-2em}.ladda-button[data-style=slide-down][data-loading] .ladda-label{opacity:0;top:1em}.ladda-button[data-style=slide-down][data-loading] .ladda-spinner{opacity:1;margin-top:0}.ladda-button[data-style=zoom-out]{overflow:hidden}.ladda-button[data-style=zoom-out] .ladda-spinner{left:50%;margin-left:32px;-webkit-transform:scale(2.5);-moz-transform:scale(2.5);-ms-transform:scale(2.5);-o-transform:scale(2.5);transform:scale(2.5)}.ladda-button[data-style=zoom-out] .ladda-label{position:relative;display:inline-block}.ladda-button[data-style=zoom-out][data-loading] .ladda-label{opacity:0;-webkit-transform:scale(0.5);-moz-transform:scale(0.5);-ms-transform:scale(0.5);-o-transform:scale(0.5);transform:scale(0.5)}.ladda-button[data-style=zoom-out][data-loading] .ladda-spinner{opacity:1;margin-left:0;-webkit-transform:none;-moz-transform:none;-ms-transform:none;-o-transform:none;transform:none}.ladda-button[data-style=zoom-in]{overflow:hidden}.ladda-button[data-style=zoom-in] .ladda-spinner{left:50%;margin-left:-16px;-webkit-transform:scale(0.2);-moz-transform:scale(0.2);-ms-transform:scale(0.2);-o-transform:scale(0.2);transform:scale(0.2)}.ladda-button[data-style=zoom-in] .ladda-label{position:relative;display:inline-block}.ladda-button[data-style=zoom-in][data-loading] .ladda-label{opacity:0;-webkit-transform:scale(2.2);-moz-transform:scale(2.2);-ms-transform:scale(2.2);-o-transform:scale(2.2);transform:scale(2.2)}.ladda-button[data-style=zoom-in][data-loading] .ladda-spinner{opacity:1;margin-left:0;-webkit-transform:none;-moz-transform:none;-ms-transform:none;-o-transform:none;transform:none}.ladda-button[data-style=contract]{overflow:hidden;width:100px}.ladda-button[data-style=contract] .ladda-spinner{left:50%;margin-left:0}.ladda-button[data-style=contract][data-loading]{border-radius:50%;width:52px}.ladda-button[data-style=contract][data-loading] .ladda-label{opacity:0}.ladda-button[data-style=contract][data-loading] .ladda-spinner{opacity:1}.ladda-button[data-style=contract-overlay]{overflow:hidden;width:100px;box-shadow:0px 0px 0px 2000px transparent}.ladda-button[data-style=contract-overlay] .ladda-spinner{left:50%;margin-left:0}.ladda-button[data-style=contract-overlay][data-loading]{border-radius:50%;width:52px;box-shadow:0px 0px 0px 2000px rgba(0,0,0,0.8)}.ladda-button[data-style=contract-overlay][data-loading] .ladda-label{opacity:0}.ladda-button[data-style=contract-overlay][data-loading] .ladda-spinner{opacity:1}.ladda-button{background:#666;border:0;padding:14px 18px;font-size:18px;cursor:pointer;color:#fff;border-radius:2px;border:1px solid transparent;-webkit-appearance:none;-webkit-font-smoothing:antialiased;-webkit-tap-highlight-color:transparent}.ladda-button:hover{border-color:rgba(0,0,0,0.07);background-color:#888}.ladda-button[data-color=green]{background:#2aca76}.ladda-button[data-color=green]:hover{background-color:#38d683}.ladda-button[data-color=blue]{background:#53b5e6}.ladda-button[data-color=blue]:hover{background-color:#69bfe9}.ladda-button[data-color=red]{background:#ea8557}.ladda-button[data-color=red]:hover{background-color:#ed956e}.ladda-button[data-color=purple]{background:#9973C2}.ladda-button[data-color=purple]:hover{background-color:#a685ca}.ladda-button[data-color=mint]{background:#16a085}.ladda-button[data-color=mint]:hover{background-color:#19b698}.ladda-button[disabled],.ladda-button[data-loading]{border-color:rgba(0,0,0,0.07)}.ladda-button[disabled],.ladda-button[disabled]:hover,.ladda-button[data-loading],.ladda-button[data-loading]:hover{cursor:default;background-color:#999}.ladda-button[data-size=xs]{padding:4px 8px}.ladda-button[data-size=xs] .ladda-label{font-size:0.7em}.ladda-button[data-size=s]{padding:6px 10px}.ladda-button[data-size=s] .ladda-label{font-size:0.9em}.ladda-button[data-size=l] .ladda-label{font-size:1.2em}.ladda-button[data-size=xl] .ladda-label{font-size:1.5em}
diff --git a/apps/static/js/jumpserver.js b/apps/static/js/jumpserver.js
index 773023f16..d334d6c64 100644
--- a/apps/static/js/jumpserver.js
+++ b/apps/static/js/jumpserver.js
@@ -158,7 +158,7 @@ function activeNav(prefix) {
} else {
$("#" + app).addClass('active');
$('#' + app + ' #' + resource).addClass('active');
- $('#' + app + ' #' + resource.replaceAll('-', '_')).addClass('active');
+ $('#' + app + ' #' + resource.replace(/-/g, '_')).addClass('active');
}
}
@@ -177,7 +177,7 @@ function formSubmit(props) {
*/
props = props || {};
var data = props.data || props.form.serializeObject();
- var redirect_to = props.redirect_to;
+ var redirectTo = props.redirect_to || props.redirectTo;
$.ajax({
url: props.url,
type: props.method || 'POST',
@@ -185,12 +185,8 @@ function formSubmit(props) {
contentType: props.content_type || "application/json; charset=utf-8",
dataType: props.data_type || "json"
}).done(function (data, textState, jqXHR) {
- if (redirect_to) {
- if (props.message) {
- var messages = "ed65330a45559c87345a0eb6ac7812d18d0d8976$[[\"__json_message\"\0540\05425\054\"asdfasdf \\u521b\\u5efa\\u6210\\u529f\"]]"
- setCookie("messages", messages)
- }
- location.href = redirect_to;
+ if (redirectTo) {
+ location.href = redirectTo;
} else if (typeof props.success === 'function') {
return props.success(data, textState, jqXHR);
}
@@ -254,7 +250,6 @@ function formSubmit(props) {
}
$('.has-error').get(0).scrollIntoView();
}
-
})
}
@@ -316,7 +311,7 @@ function requestApi(props) {
}
// Sweet Alert for Delete
-function objectDelete(obj, name, url, redirectTo) {
+function objectDelete(obj, name, url, redirectTo, title, success_message) {
function doDelete() {
var body = {};
var success = function () {
@@ -335,14 +330,14 @@ function objectDelete(obj, name, url, redirectTo) {
url: url,
body: JSON.stringify(body),
method: 'DELETE',
- success_message: gettext("Delete the success"),
+ success_message: success_message || gettext("Delete the success"),
success: success,
error: fail
});
}
swal({
- title: gettext('Are you sure about deleting it?'),
+ title: title || gettext('Are you sure about deleting it?'),
text: " [" + name + "] ",
type: "warning",
showCancelButton: true,
@@ -413,7 +408,7 @@ $.fn.serializeObject = function () {
};
function makeLabel(data) {
- return "
" + data[0] + ": " + data[1] + ""
+ return "
" + data[0] + ": " + data[1] + ""
}
function parseTableFilter(value) {
@@ -600,6 +595,7 @@ jumpserver.initServerSideDataTable = function (options) {
// op_html: 'div.btn-group?',
// paging: true,
// paging_numbers_length: 5;
+ // hideDefaultDefs: false;
// }
var pagingNumbersLength = 5;
if (options.paging_numbers_length){
@@ -613,7 +609,8 @@ jumpserver.initServerSideDataTable = function (options) {
orderable: false,
width: "20px",
createdCell: function (td, cellData) {
- $(td).html('
'.replace('99991937', cellData));
+ var data = '
'.replace('Id', cellData);
+ $(td).html(data);
}
},
{
@@ -622,6 +619,9 @@ jumpserver.initServerSideDataTable = function (options) {
render: $.fn.dataTable.render.text()
}
];
+ if (options.hideDefaultDefs) {
+ columnDefs = [];
+ }
var select_style = options.select_style || 'multi';
columnDefs = options.columnDefs ? options.columnDefs.concat(columnDefs) : columnDefs;
var select = {
@@ -635,7 +635,7 @@ jumpserver.initServerSideDataTable = function (options) {
pageLength: options.pageLength || 15,
// dom: options.dom || '<"#uc.pull-left">fltr<"row m-t"<"col-md-8"<"#op.col-md-6"><"col-md-6 text-center"i>><"col-md-4"p>>',
// dom: options.dom || '<"#uc.pull-left"><"pull-right"<"inline"l><"#fb.inline"><"inline"<"table-filter"f>><"#fa.inline">>tr<"row m-t"<"col-md-8"<"#op.col-md-6"><"col-md-6 text-center"i>><"col-md-4"p>>',
- dom: dom,
+ dom: options.dom || dom,
order: options.order || [],
buttons: [],
columnDefs: columnDefs,
@@ -713,8 +713,14 @@ jumpserver.initServerSideDataTable = function (options) {
var rows = table.rows(indexes).data();
$.each(rows, function (id, row) {
if (row.id && $.inArray(row.id, table.selected) === -1) {
- table.selected.push(row.id);
- table.selected_rows.push(row);
+ if (select.style === 'multi'){
+ table.selected.push(row.id);
+ table.selected_rows.push(row);
+ }
+ else{
+ table.selected = [row.id];
+ table.selected_rows = [row];
+ }
}
})
}
@@ -1027,6 +1033,62 @@ function rootNodeAddDom(ztree, callback) {
})
}
+function APIExportCSV(props) {
+ /*
+ {
+ listUrl:
+ objectsId:
+ template:
+ table:
+ params:
+ }
+ */
+ var _listUrl = props.listUrl;
+ var _objectsId = props.objectsId;
+ var _template = props.template;
+ var _table = props.table;
+ var _params = props.params || {};
+
+ var tableParams = _table.ajax.params();
+ var exportUrl = setUrlParam(_listUrl, 'format', 'csv');
+ if (_template) {
+ exportUrl = setUrlParam(exportUrl, 'template', _template)
+ }
+ for (var k in tableParams) {
+ if (datatableInternalParams.includes(k)) {
+ continue
+ }
+ if (!tableParams[k]) {
+ continue
+ }
+ exportUrl = setUrlParam(exportUrl, k, tableParams[k])
+ }
+ for (var k in _params) {
+ exportUrl = setUrlParam(exportUrl, k, tableParams[k])
+ }
+
+ if (!_objectsId) {
+ console.log(exportUrl);
+ window.open(exportUrl);
+ return
+ }
+
+ requestApi({
+ url: '/api/v1/common/resources/cache/',
+ data: JSON.stringify({resources: _objectsId}),
+ method: "POST",
+ flash_message: false,
+ success: function (data) {
+ exportUrl = setUrlParam(exportUrl, 'spm', data.spm);
+ console.log(exportUrl);
+ window.open(exportUrl);
+ },
+ failed: function () {
+ toastr.error(gettext('Export failed'));
+ }
+ });
+}
+
function APIExportData(props) {
props = props || {};
$.ajax({
@@ -1076,6 +1138,7 @@ function APIImportData(props) {
},
error: function (error) {
var data = error.responseJSON;
+ console.log(data);
if (data instanceof Array) {
var html = '';
var li = '';
@@ -1136,8 +1199,8 @@ function objectAttrsIsBool(obj, attrs) {
attrs.forEach(function (attr) {
if (!obj[attr]) {
obj[attr] = false
- } else if (['on', '1'].includes(obj[attr])) {
- obj[attr] = true
+ } else {
+ obj[attr] = ['on', '1', 'true', 'True'].includes(obj[attr]);
}
})
}
@@ -1241,10 +1304,28 @@ function readFile(ref) {
return ref
}
-function nodesSelect2Init(selector, url) {
- if (!url) {
- url = '/api/v1/assets/nodes/'
+
+
+function select2AjaxInit(option) {
+ /*
+ {
+ selector:
+ url: ,
+ disabledData: ,
+ displayFormat,
+ idFormat,
}
+ */
+ var selector = option.selector;
+ var url = option.url;
+ var disabledData = option.disabledData;
+ var displayFormat = option.displayFormat || function (data) {
+ return data.name;
+ };
+ var idFormat = option.idFormat || function (data) {
+ return data.id;
+ };
+
return $(selector).select2({
closeOnSelect: false,
ajax: {
@@ -1260,43 +1341,53 @@ function nodesSelect2Init(selector, url) {
},
processResults: function (data) {
var results = $.map(data.results, function (v, i) {
- return {id: v.id, text: v.full_value}
+ var display = displayFormat(v);
+ var id = idFormat(v);
+ var d = {id: id, text: display};
+ if (disabledData && disabledData.indexOf(v.id) !== -1) {
+ d.disabled = true;
+ }
+ return d;
});
var more = !!data.next;
return {results: results, pagination: {"more": more}}
}
},
})
+
}
-function usersSelect2Init(selector, url) {
+function usersSelect2Init(selector, url, disabledData) {
if (!url) {
url = '/api/v1/users/users/'
}
- return $(selector).select2({
- closeOnSelect: false,
- ajax: {
- url: url,
- data: function (params) {
- var page = params.page || 1;
- var query = {
- search: params.term,
- offset: (page - 1) * 10,
- limit: 10
- };
- return query
- },
- processResults: function (data) {
- var results = $.map(data.results, function (v, i) {
- var display = v.name + '(' + v.username +')';
- return {id: v.id, text: display}
- });
- var more = !!data.next;
- return {results: results, pagination: {"more": more}}
- }
- },
- })
+ function displayFormat(v) {
+ return v.name + '(' + v.username +')';
+ }
+ var option = {
+ url: url,
+ selector: selector,
+ disabledData: disabledData,
+ displayFormat: displayFormat
+ };
+ return select2AjaxInit(option)
+}
+
+function nodesSelect2Init(selector, url, disabledData) {
+ if (!url) {
+ url = '/api/v1/assets/nodes/'
+ }
+ function displayFormat(v) {
+ return v.full_value;
+ }
+ var option = {
+ url: url,
+ selector: selector,
+ disabledData: disabledData,
+ displayFormat: displayFormat
+ };
+ return select2AjaxInit(option)
}
function showCeleryTaskLog(taskId) {
@@ -1324,7 +1415,7 @@ function initDateRangePicker(selector, options) {
timePicker24Hour: true,
autoApply: true,
};
- var userLang = navigator.language || navigator.userLanguage;;
+ var userLang = navigator.language || navigator.userLanguage;
if (userLang.indexOf('zh') !== -1) {
defaultOption.locale = zhLocale;
}
diff --git a/apps/static/js/plugins/ladda/ladda.jquery.min.js b/apps/static/js/plugins/ladda/ladda.jquery.min.js
new file mode 100755
index 000000000..74fb3ae03
--- /dev/null
+++ b/apps/static/js/plugins/ladda/ladda.jquery.min.js
@@ -0,0 +1,8 @@
+/*!
+ * Ladda for jQuery
+ * http://lab.hakim.se/ladda
+ * MIT licensed
+ *
+ * Copyright (C) 2015 Hakim El Hattab, http://hakim.se
+ */
+!function(a,b){if(void 0===b)return console.error("jQuery required for Ladda.jQuery");var c=[];b=b.extend(b,{ladda:function(b){"stopAll"===b&&a.stopAll()}}),b.fn=b.extend(b.fn,{ladda:function(d){var e=c.slice.call(arguments,1);return"bind"===d?(e.unshift(b(this).selector),a.bind.apply(a,e)):b(this).each(function(){var c,f=b(this);void 0===d?f.data("ladda",a.create(this)):(c=f.data("ladda"),c[d].apply(c,e))}),this}})}(this.Ladda,this.jQuery);
\ No newline at end of file
diff --git a/apps/static/js/plugins/ladda/ladda.min.js b/apps/static/js/plugins/ladda/ladda.min.js
new file mode 100755
index 000000000..f7f81ec74
--- /dev/null
+++ b/apps/static/js/plugins/ladda/ladda.min.js
@@ -0,0 +1,8 @@
+/*!
+ * Ladda 1.0.0 (2016-03-08, 09:31)
+ * http://lab.hakim.se/ladda
+ * MIT licensed
+ *
+ * Copyright (C) 2016 Hakim El Hattab, http://hakim.se
+ */
+!function(a,b){"object"==typeof exports?module.exports=b(require("spin.js")):"function"==typeof define&&define.amd?define(["spin"],b):a.Ladda=b(a.Spinner)}(this,function(a){"use strict";function b(a){if("undefined"==typeof a)return void console.warn("Ladda button target must be defined.");if(/ladda-button/i.test(a.className)||(a.className+=" ladda-button"),a.hasAttribute("data-style")||a.setAttribute("data-style","expand-right"),!a.querySelector(".ladda-label")){var b=document.createElement("span");b.className="ladda-label",i(a,b)}var c,d=a.querySelector(".ladda-spinner");d||(d=document.createElement("span"),d.className="ladda-spinner"),a.appendChild(d);var e,f={start:function(){return c||(c=g(a)),a.setAttribute("disabled",""),a.setAttribute("data-loading",""),clearTimeout(e),c.spin(d),this.setProgress(0),this},startAfter:function(a){return clearTimeout(e),e=setTimeout(function(){f.start()},a),this},stop:function(){return a.removeAttribute("disabled"),a.removeAttribute("data-loading"),clearTimeout(e),c&&(e=setTimeout(function(){c.stop()},1e3)),this},toggle:function(){return this.isLoading()?this.stop():this.start(),this},setProgress:function(b){b=Math.max(Math.min(b,1),0);var c=a.querySelector(".ladda-progress");0===b&&c&&c.parentNode?c.parentNode.removeChild(c):(c||(c=document.createElement("div"),c.className="ladda-progress",a.appendChild(c)),c.style.width=(b||0)*a.offsetWidth+"px")},enable:function(){return this.stop(),this},disable:function(){return this.stop(),a.setAttribute("disabled",""),this},isLoading:function(){return a.hasAttribute("data-loading")},remove:function(){clearTimeout(e),a.removeAttribute("disabled",""),a.removeAttribute("data-loading",""),c&&(c.stop(),c=null);for(var b=0,d=j.length;d>b;b++)if(f===j[b]){j.splice(b,1);break}}};return j.push(f),f}function c(a,b){for(;a.parentNode&&a.tagName!==b;)a=a.parentNode;return b===a.tagName?a:void 0}function d(a){for(var b=["input","textarea","select"],c=[],d=0;d
g;g++)!function(){var a=f[g];if("function"==typeof a.addEventListener){var h=b(a),i=-1;a.addEventListener("click",function(b){var f=!0,g=c(a,"FORM");if("undefined"!=typeof g)if("function"==typeof g.checkValidity)f=g.checkValidity();else for(var j=d(g),k=0;ka;a++)j[a].stop()}function g(b){var c,d,e=b.offsetHeight;0===e&&(e=parseFloat(window.getComputedStyle(b).height)),e>32&&(e*=.8),b.hasAttribute("data-spinner-size")&&(e=parseInt(b.getAttribute("data-spinner-size"),10)),b.hasAttribute("data-spinner-color")&&(c=b.getAttribute("data-spinner-color")),b.hasAttribute("data-spinner-lines")&&(d=parseInt(b.getAttribute("data-spinner-lines"),10));var f=.2*e,g=.6*f,h=7>f?2:3;return new a({color:c||"#fff",lines:d||12,radius:f,length:g,width:h,zIndex:"auto",top:"auto",left:"auto",className:""})}function h(a){for(var b=[],c=0;cb;b++)a.appendChild(arguments[b]);return a}function c(a,b,c,d){var e=["opacity",b,~~(100*a),c,d].join("-"),f=.01+c/d*100,g=Math.max(1-(1-a)/b*(100-f),a),h=j.substring(0,j.indexOf("Animation")).toLowerCase(),i=h&&"-"+h+"-"||"";return l[e]||(m.insertRule("@"+i+"keyframes "+e+"{0%{opacity:"+g+"}"+f+"%{opacity:"+a+"}"+(f+.01)+"%{opacity:1}"+(f+b)%100+"%{opacity:"+a+"}100%{opacity:"+g+"}}",m.cssRules.length),l[e]=1),e}function d(a,b){var c,d,e=a.style;for(b=b.charAt(0).toUpperCase()+b.slice(1),d=0;d',c)}m.addRule(".spin-vml","behavior:url(#default#VML)"),h.prototype.lines=function(a,d){function f(){return e(c("group",{coordsize:k+" "+k,coordorigin:-j+" "+-j}),{width:k,height:k})}function h(a,h,i){b(m,b(e(f(),{rotation:360/d.lines*a+"deg",left:~~h}),b(e(c("roundrect",{arcsize:d.corners}),{width:j,height:d.width,left:d.radius,top:-d.width>>1,filter:i}),c("fill",{color:g(d.color,a),opacity:d.opacity}),c("stroke",{opacity:0}))))}var i,j=d.length+d.width,k=2*j,l=2*-(d.width+d.length)+"px",m=e(f(),{position:"absolute",top:l,left:l});if(d.shadow)for(i=1;i<=d.lines;i++)h(i,-2,"progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)");for(i=1;i<=d.lines;i++)h(i);return b(a,m)},h.prototype.opacity=function(a,b,c,d){var e=a.firstChild;d=d.shadow&&d.lines||0,e&&b+d>1)+"px"})}for(var i,k=0,l=(f.lines-1)*(1-f.direction)/2;k
+
+
+ {% include 'assets/_node_tree.html' %}
+
+
+
+
+
+{% endblock %}
diff --git a/apps/templates/_base_create_update.html b/apps/templates/_base_create_update.html
index be3813804..d206d40b2 100644
--- a/apps/templates/_base_create_update.html
+++ b/apps/templates/_base_create_update.html
@@ -3,8 +3,6 @@
{% load static %}
{% load bootstrap3 %}
{% block custom_head_css_js %}
-
-
{% block custom_head_css_js_create %} {% endblock %}
{% endblock %}
diff --git a/apps/templates/_base_list.html b/apps/templates/_base_list.html
index c5314af4e..9759081bd 100644
--- a/apps/templates/_base_list.html
+++ b/apps/templates/_base_list.html
@@ -1,10 +1,6 @@
{% extends 'base.html' %}
{% load static %}
{% load i18n %}
-{% block custom_head_css_js %}
-
-
-{% endblock %}
{% block content %}
diff --git a/apps/templates/_base_only_content.html b/apps/templates/_base_only_content.html
new file mode 100644
index 000000000..1e6a16c84
--- /dev/null
+++ b/apps/templates/_base_only_content.html
@@ -0,0 +1,47 @@
+{% load static %}
+{% load i18n %}
+
+
+
+
+
+
+
+
{% block html_title %}{% endblock %}
+
+ {% include '_head_css_js.html' %}
+
+
+
+
+ {% block custom_head_css_js %} {% endblock %}
+
+
+
+
+
+
+
+
+
{% block title %}{% endblock %}
+
+ {% block content %} {% endblock %}
+
+
+
+
+
+
+ {% include '_copyright.html' %}
+
+
+
+
+{% block custom_foot_js %} {% endblock %}
+
diff --git a/apps/templates/_csv_import_export.html b/apps/templates/_csv_import_export.html
new file mode 100644
index 000000000..d22c947b4
--- /dev/null
+++ b/apps/templates/_csv_import_export.html
@@ -0,0 +1,62 @@
+{% load i18n %}
+
+{% include '_csv_import_modal.html' %}
+{% include '_csv_update_modal.html' %}
+
+
diff --git a/apps/templates/_csv_import_modal.html b/apps/templates/_csv_import_modal.html
new file mode 100644
index 000000000..4925793d0
--- /dev/null
+++ b/apps/templates/_csv_import_modal.html
@@ -0,0 +1,52 @@
+{% extends '_modal.html' %}
+{% load i18n %}
+
+{% block modal_id %}csv_import_modal{% endblock %}
+{% block modal_title%}
csv {% trans 'Import' %}{% endblock %}
+{% block modal_confirm_id %}btn_csv_import_confirm{% endblock %}
+
+{% block modal_body %}
+
+ {% csrf_token %}
+
+
+
+ {% trans "Select the CSV file to import" %}
+
+
+
+
+
+
+
+{% endblock %}
+
+
diff --git a/apps/templates/_csv_update_modal.html b/apps/templates/_csv_update_modal.html
new file mode 100644
index 000000000..c4c31abda
--- /dev/null
+++ b/apps/templates/_csv_update_modal.html
@@ -0,0 +1,54 @@
+{% extends '_modal.html' %}
+{% load i18n %}
+
+{% block modal_id %}csv_update_modal{% endblock %}
+{% block modal_confirm_id %}btn_csv_update_confirm{% endblock %}
+{% block modal_title%}
csv {% trans 'Update' %}{% endblock %}
+
+{% block modal_body %}
+
+ {% csrf_token %}
+
+
+
+ {% trans "Select the CSV file to import" %}
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/apps/templates/_foot_js.html b/apps/templates/_foot_js.html
index 509b23ad3..22161696f 100644
--- a/apps/templates/_foot_js.html
+++ b/apps/templates/_foot_js.html
@@ -7,7 +7,9 @@
-
+
+
+
diff --git a/apps/templates/_head_css_js.html b/apps/templates/_head_css_js.html
index 45694e6fc..b4fbcd47b 100644
--- a/apps/templates/_head_css_js.html
+++ b/apps/templates/_head_css_js.html
@@ -13,3 +13,5 @@
+
+
diff --git a/apps/templates/_import_modal.html b/apps/templates/_import_modal.html
deleted file mode 100644
index 9211bdcb9..000000000
--- a/apps/templates/_import_modal.html
+++ /dev/null
@@ -1,28 +0,0 @@
-{% extends '_modal.html' %}
-{% load i18n %}
-
-{% block modal_id %}import_modal{% endblock %}
-
-{% block modal_confirm_id %}btn_import_confirm{% endblock %}
-
-{% block modal_body %}
-
- {% csrf_token %}
-
-
-
- {% trans "Select the CSV file to import" %}
-
-
-
-
-
-{% endblock %}
diff --git a/apps/templates/_nav.html b/apps/templates/_nav.html
index 03f9d83cb..5ad4a2618 100644
--- a/apps/templates/_nav.html
+++ b/apps/templates/_nav.html
@@ -45,6 +45,9 @@
{% trans 'System user' %}
{% trans 'Labels' %}
{% trans 'Command filters' %}
+ {% if request.user.is_superuser %}
+
{% trans 'Platform list' %}
+ {% endif %}
{% endif %}
@@ -58,6 +61,7 @@
{% endif %}
@@ -75,6 +79,9 @@
{% trans 'RemoteApp' %}
+
+ {% trans 'DatabaseApp' %}
+
{% endif %}
@@ -113,7 +120,7 @@
{% endif %}
{% if SECURITY_COMMAND_EXECUTION %}
-
+
{% trans 'Command execution' %}
@@ -41,4 +46,4 @@
{% trans 'File manager' %}
-
\ No newline at end of file
+
diff --git a/apps/templates/_update_modal.html b/apps/templates/_update_modal.html
deleted file mode 100644
index db2b14110..000000000
--- a/apps/templates/_update_modal.html
+++ /dev/null
@@ -1,28 +0,0 @@
-{% extends '_modal.html' %}
-{% load i18n %}
-
-{% block modal_id %}update_modal{% endblock %}
-
-{% block modal_confirm_id %}btn_update_confirm{% endblock %}
-
-{% block modal_body %}
-
- {% csrf_token %}
-
-
-
- {% trans "Select the CSV file to import" %}
-
-
-
-
-
-{% endblock %}
diff --git a/apps/templates/_without_nav_base.html b/apps/templates/_without_nav_base.html
new file mode 100644
index 000000000..98bcb6189
--- /dev/null
+++ b/apps/templates/_without_nav_base.html
@@ -0,0 +1,44 @@
+{% load static %}
+{% load i18n %}
+
+
+
+
+
+
{{ JMS_TITLE }}
+
+{#
#}
+
+
+
+
+
+
+
+
+
+ {% block body %}
+ {% endblock %}
+
+
+
+
+
diff --git a/apps/templates/flash_message_standalone.html b/apps/templates/flash_message_standalone.html
index 304a8bd0a..84ef58cb5 100644
--- a/apps/templates/flash_message_standalone.html
+++ b/apps/templates/flash_message_standalone.html
@@ -1,83 +1,64 @@
-{% load i18n %}
+{% extends '_base_only_content.html' %}
{% load static %}
-
-
+{% load i18n %}
+{% block html_title %} {{ title }} {% endblock %}
+{% block title %} {{ title }}{% endblock %}
-
-
-
+{% block custom_head_css_js %}
+
+{% endblock %}
-
{{ title }}
-
- {% include '_head_css_js.html' %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ JMS_TITLE }}
-
-
- {% if errors %}
-
-
- {{ errors }}
-
-
- {% endif %}
-
- {% if messages %}
-
-
- {{ messages|safe }}
-
-
- {% endif %}
-
-
+{% block content %}
+
+ {% if errors %}
+
+
+ {{ errors }}
-
-
+
+ {% endif %}
+
+ {% if messages %}
+
+
+ {{ messages|safe }}
+
+
+ {% endif %}
-
- {% include '_copyright.html' %}
+
-
+{% endblock %}
+
+{% block custom_foot_js %}
-
+{% endblock %}
+
diff --git a/apps/templates/index.html b/apps/templates/index.html
index c97b6e849..76b6ddddd 100644
--- a/apps/templates/index.html
+++ b/apps/templates/index.html
@@ -477,4 +477,4 @@ $(document).ready(function(){
);
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/apps/terminal/api/__init__.py b/apps/terminal/api/__init__.py
index d33905b78..640dacb6a 100644
--- a/apps/terminal/api/__init__.py
+++ b/apps/terminal/api/__init__.py
@@ -4,3 +4,4 @@ from .terminal import *
from .session import *
from .command import *
from .task import *
+from .storage import *
diff --git a/apps/terminal/api/session.py b/apps/terminal/api/session.py
index 264502959..7607d46de 100644
--- a/apps/terminal/api/session.py
+++ b/apps/terminal/api/session.py
@@ -12,10 +12,10 @@ import jms_storage
from common.utils import is_uuid, get_logger
from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor
-from common.filters import DatetimeRangeFilter
+from common.drf.filters import DatetimeRangeFilter
from orgs.mixins.api import OrgBulkModelViewSet
from ..hands import SystemUser
-from ..models import Session
+from ..models import Session, ReplayStorage
from .. import serializers
@@ -105,9 +105,12 @@ class SessionReplayViewSet(viewsets.ViewSet):
data['src'] = url
return Response(data)
- # 去定义的外部storage查找
- configs = settings.TERMINAL_REPLAY_STORAGE
- configs = {k: v for k, v in configs.items() if v['TYPE'] != 'server'}
+ replay_storages = ReplayStorage.objects.all()
+ configs = {
+ storage.name: storage.config
+ for storage in replay_storages
+ if not storage.in_defaults()
+ }
if not configs:
return HttpResponseNotFound()
diff --git a/apps/terminal/api/storage.py b/apps/terminal/api/storage.py
new file mode 100644
index 000000000..ec85b95f7
--- /dev/null
+++ b/apps/terminal/api/storage.py
@@ -0,0 +1,74 @@
+# coding: utf-8
+#
+
+from rest_framework import viewsets, generics, status
+from rest_framework.response import Response
+from django.utils.translation import ugettext_lazy as _
+
+from common.permissions import IsSuperUser
+from ..models import CommandStorage, ReplayStorage
+from ..serializers import CommandStorageSerializer, ReplayStorageSerializer
+
+
+__all__ = [
+ 'CommandStorageViewSet', 'CommandStorageTestConnectiveApi',
+ 'ReplayStorageViewSet', 'ReplayStorageTestConnectiveApi'
+]
+
+
+class BaseStorageViewSetMixin:
+
+ def destroy(self, request, *args, **kwargs):
+ instance = self.get_object()
+ if not instance.can_delete():
+ data = {'msg': _('Deleting the default storage is not allowed')}
+ return Response(data=data, status=status.HTTP_400_BAD_REQUEST)
+ return super().destroy(request, *args, **kwargs)
+
+
+class CommandStorageViewSet(BaseStorageViewSetMixin, viewsets.ModelViewSet):
+ filter_fields = ('name', 'type',)
+ search_fields = filter_fields
+ queryset = CommandStorage.objects.all()
+ serializer_class = CommandStorageSerializer
+ permission_classes = (IsSuperUser,)
+
+
+class ReplayStorageViewSet(BaseStorageViewSetMixin, viewsets.ModelViewSet):
+ filter_fields = ('name', 'type',)
+ search_fields = filter_fields
+ queryset = ReplayStorage.objects.all()
+ serializer_class = ReplayStorageSerializer
+ permission_classes = (IsSuperUser,)
+
+
+class BaseStorageTestConnectiveMixin:
+ permission_classes = (IsSuperUser,)
+
+ def retrieve(self, request, *args, **kwargs):
+ instance = self.get_object()
+ try:
+ is_valid = instance.is_valid()
+ except Exception as e:
+ is_valid = False
+ msg = _("Test failure: {}".format(str(e)))
+ else:
+ if is_valid:
+ msg = _("Test successful")
+ else:
+ msg = _("Test failure: Account invalid")
+ data = {
+ 'is_valid': is_valid,
+ 'msg': msg
+ }
+ return Response(data)
+
+
+class CommandStorageTestConnectiveApi(BaseStorageTestConnectiveMixin,
+ generics.RetrieveAPIView):
+ queryset = CommandStorage.objects.all()
+
+
+class ReplayStorageTestConnectiveApi(BaseStorageTestConnectiveMixin,
+ generics.RetrieveAPIView):
+ queryset = ReplayStorage.objects.all()
diff --git a/apps/terminal/backends/__init__.py b/apps/terminal/backends/__init__.py
index 1aec6b3be..b2a9c557c 100644
--- a/apps/terminal/backends/__init__.py
+++ b/apps/terminal/backends/__init__.py
@@ -1,8 +1,8 @@
from importlib import import_module
from django.conf import settings
from .command.serializers import SessionCommandSerializer
+from ..const import COMMAND_STORAGE_TYPE_SERVER
-from common import utils
TYPE_ENGINE_MAPPING = {
'elasticsearch': 'terminal.backends.command.es',
@@ -18,19 +18,18 @@ def get_command_storage():
def get_terminal_command_storages():
+ from ..models import CommandStorage
storage_list = {}
- command_storage = utils.get_command_storage_setting()
-
- for name, params in command_storage.items():
- tp = params['TYPE']
- if tp == 'server':
+ for s in CommandStorage.objects.all():
+ tp = s.type
+ if tp == COMMAND_STORAGE_TYPE_SERVER:
storage = get_command_storage()
else:
if not TYPE_ENGINE_MAPPING.get(tp):
continue
engine_class = import_module(TYPE_ENGINE_MAPPING[tp])
- storage = engine_class.CommandStore(params)
- storage_list[name] = storage
+ storage = engine_class.CommandStore(s.config)
+ storage_list[s.name] = storage
return storage_list
diff --git a/apps/terminal/const.py b/apps/terminal/const.py
index 2d74e00c1..b412c1624 100644
--- a/apps/terminal/const.py
+++ b/apps/terminal/const.py
@@ -5,3 +5,105 @@ ASSETS_CACHE_KEY = "terminal__session__assets"
USERS_CACHE_KEY = "terminal__session__users"
SYSTEM_USER_CACHE_KEY = "terminal__session__system_users"
+
+# Replay Storage
+
+REPLAY_STORAGE_TYPE_NULL = 'null'
+REPLAY_STORAGE_TYPE_SERVER = 'server'
+REPLAY_STORAGE_TYPE_S3 = 's3'
+REPLAY_STORAGE_TYPE_CEPH = 'ceph'
+REPLAY_STORAGE_TYPE_SWIFT = 'swift'
+REPLAY_STORAGE_TYPE_OSS = 'oss'
+REPLAY_STORAGE_TYPE_AZURE = 'azure'
+
+REPLAY_STORAGE_TYPE_EMPTY_FIELDS = []
+REPLAY_STORAGE_TYPE_S3_FIELDS = [
+ {'name': 'BUCKET'},
+ {'name': 'ACCESS_KEY', 'write_only': True},
+ {'name': 'SECRET_KEY', 'write_only': True},
+ {'name': 'ENDPOINT'}
+]
+REPLAY_STORAGE_TYPE_CEPH_FIELDS = [
+ {'name': 'BUCKET'},
+ {'name': 'ACCESS_KEY', 'write_only': True},
+ {'name': 'SECRET_KEY', 'write_only': True},
+ {'name': 'ENDPOINT'}
+]
+REPLAY_STORAGE_TYPE_SWIFT_FIELDS = [
+ {'name': 'BUCKET'},
+ {'name': 'ACCESS_KEY', 'write_only': True},
+ {'name': 'SECRET_KEY', 'write_only': True},
+ {'name': 'REGION'},
+ {'name': 'ENDPOINT'},
+]
+REPLAY_STORAGE_TYPE_OSS_FIELDS = [
+ {'name': 'BUCKET'},
+ {'name': 'ACCESS_KEY', 'write_only': True},
+ {'name': 'SECRET_KEY', 'write_only': True},
+ {'name': 'ENDPOINT'}
+]
+REPLAY_STORAGE_TYPE_AZURE_FIELDS = [
+ {'name': 'CONTAINER_NAME'},
+ {'name': 'ACCOUNT_NAME'},
+ {'name': 'ACCOUNT_KEY', 'write_only': True},
+ {'name': 'ENDPOINT_SUFFIX'}
+]
+
+REPLAY_STORAGE_TYPE_FIELDS_MAP = {
+ REPLAY_STORAGE_TYPE_NULL: REPLAY_STORAGE_TYPE_EMPTY_FIELDS,
+ REPLAY_STORAGE_TYPE_SERVER: REPLAY_STORAGE_TYPE_EMPTY_FIELDS,
+ REPLAY_STORAGE_TYPE_S3: REPLAY_STORAGE_TYPE_S3_FIELDS,
+ REPLAY_STORAGE_TYPE_CEPH: REPLAY_STORAGE_TYPE_CEPH_FIELDS,
+ REPLAY_STORAGE_TYPE_SWIFT: REPLAY_STORAGE_TYPE_SWIFT_FIELDS,
+ REPLAY_STORAGE_TYPE_OSS: REPLAY_STORAGE_TYPE_OSS_FIELDS,
+ REPLAY_STORAGE_TYPE_AZURE: REPLAY_STORAGE_TYPE_AZURE_FIELDS
+}
+
+REPLAY_STORAGE_TYPE_CHOICES_DEFAULT = [
+ (REPLAY_STORAGE_TYPE_NULL, 'Null'),
+ (REPLAY_STORAGE_TYPE_SERVER, 'Server'),
+]
+
+REPLAY_STORAGE_TYPE_CHOICES_EXTENDS = [
+ (REPLAY_STORAGE_TYPE_S3, 'S3'),
+ (REPLAY_STORAGE_TYPE_CEPH, 'Ceph'),
+ (REPLAY_STORAGE_TYPE_SWIFT, 'Swift'),
+ (REPLAY_STORAGE_TYPE_OSS, 'OSS'),
+ (REPLAY_STORAGE_TYPE_AZURE, 'Azure')
+]
+
+REPLAY_STORAGE_TYPE_CHOICES = REPLAY_STORAGE_TYPE_CHOICES_DEFAULT + \
+ REPLAY_STORAGE_TYPE_CHOICES_EXTENDS
+
+
+# Command Storage
+
+COMMAND_STORAGE_TYPE_NULL = 'null'
+COMMAND_STORAGE_TYPE_SERVER = 'server'
+COMMAND_STORAGE_TYPE_ES = 'es'
+
+COMMAND_STORAGE_TYPE_EMPTY_FIELDS = []
+COMMAND_STORAGE_TYPE_ES_FIELDS = [
+ {'name': 'HOSTS'},
+ {'name': 'INDEX'},
+ {'name': 'DOC_TYPE'}
+]
+
+COMMAND_STORAGE_TYPE_FIELDS_MAP = {
+ COMMAND_STORAGE_TYPE_NULL: COMMAND_STORAGE_TYPE_EMPTY_FIELDS,
+ COMMAND_STORAGE_TYPE_SERVER: COMMAND_STORAGE_TYPE_EMPTY_FIELDS,
+ COMMAND_STORAGE_TYPE_ES: COMMAND_STORAGE_TYPE_ES_FIELDS,
+}
+
+COMMAND_STORAGE_TYPE_CHOICES_DEFAULT = [
+ (COMMAND_STORAGE_TYPE_NULL, 'Null'),
+ (COMMAND_STORAGE_TYPE_SERVER, 'Server'),
+]
+
+COMMAND_STORAGE_TYPE_CHOICES_EXTENDS = [
+ (COMMAND_STORAGE_TYPE_ES, 'Elasticsearch')
+]
+
+COMMAND_STORAGE_TYPE_CHOICES = COMMAND_STORAGE_TYPE_CHOICES_DEFAULT + \
+ COMMAND_STORAGE_TYPE_CHOICES_EXTENDS
+
diff --git a/apps/terminal/forms/__init__.py b/apps/terminal/forms/__init__.py
new file mode 100644
index 000000000..23c06a94c
--- /dev/null
+++ b/apps/terminal/forms/__init__.py
@@ -0,0 +1,5 @@
+# coding: utf-8
+#
+
+from .terminal import *
+from .storage import *
diff --git a/apps/terminal/forms/storage.py b/apps/terminal/forms/storage.py
new file mode 100644
index 000000000..28092380e
--- /dev/null
+++ b/apps/terminal/forms/storage.py
@@ -0,0 +1,166 @@
+# coding: utf-8
+#
+
+from django import forms
+from django.utils.translation import ugettext_lazy as _
+
+from terminal.models import ReplayStorage, CommandStorage
+
+
+__all__ = [
+ 'ReplayStorageAzureForm', 'ReplayStorageOSSForm', 'ReplayStorageS3Form',
+ 'ReplayStorageCephForm', 'ReplayStorageSwiftForm',
+ 'CommandStorageTypeESForm',
+]
+
+
+class BaseStorageForm(forms.Form):
+
+ def __init__(self, *args, **kwargs):
+ super(BaseStorageForm, self).__init__(*args, **kwargs)
+ self.fields['type'].widget.attrs['disabled'] = True
+ self.fields.move_to_end('comment')
+
+
+class BaseReplayStorageForm(BaseStorageForm, forms.ModelForm):
+
+ class Meta:
+ model = ReplayStorage
+ fields = ['name', 'type', 'comment']
+
+
+class BaseCommandStorageForm(BaseStorageForm, forms.ModelForm):
+
+ class Meta:
+ model = CommandStorage
+ fields = ['name', 'type', 'comment']
+
+
+class ReplayStorageAzureForm(BaseReplayStorageForm):
+ azure_container_name = forms.CharField(
+ max_length=128, label=_('Container name'), required=False
+ )
+ azure_account_name = forms.CharField(
+ max_length=128, label=_('Account name'), required=False
+ )
+ azure_account_key = forms.CharField(
+ max_length=128, label=_('Account key'), required=False,
+ widget=forms.PasswordInput
+ )
+ azure_endpoint_suffix = forms.ChoiceField(
+ choices=(
+ ('core.chinacloudapi.cn', 'core.chinacloudapi.cn'),
+ ('core.windows.net', 'core.windows.net')
+ ),
+ label=_('Endpoint suffix'), required=False,
+ )
+
+
+class ReplayStorageOSSForm(BaseReplayStorageForm):
+ oss_bucket = forms.CharField(
+ max_length=128, label=_('Bucket'), required=False
+ )
+ oss_access_key = forms.CharField(
+ max_length=128, label=_('Access key'), required=False,
+ widget=forms.PasswordInput
+ )
+ oss_secret_key = forms.CharField(
+ max_length=128, label=_('Secret key'), required=False,
+ widget=forms.PasswordInput
+ )
+ oss_endpoint = forms.CharField(
+ max_length=128, label=_('Endpoint'), required=False,
+ help_text=_(
+ """
+ OSS: http://{REGION_NAME}.aliyuncs.com
+ Example: http://oss-cn-hangzhou.aliyuncs.com
+ """
+ )
+ )
+
+
+class ReplayStorageS3Form(BaseReplayStorageForm):
+ s3_bucket = forms.CharField(
+ max_length=128, label=_('Bucket'), required=False
+ )
+ s3_access_key = forms.CharField(
+ max_length=128, label=_('Access key'), required=False,
+ widget=forms.PasswordInput
+ )
+ s3_secret_key = forms.CharField(
+ max_length=128, label=_('Secret key'), required=False,
+ widget=forms.PasswordInput
+ )
+ s3_endpoint = forms.CharField(
+ max_length=128, label=_('Endpoint'), required=False,
+ help_text=_(
+ """
+ S3: http://s3.{REGION_NAME}.amazonaws.com
+ S3(China): http://s3.{REGION_NAME}.amazonaws.com.cn
+ Example: http://s3.cn-north-1.amazonaws.com.cn
+ """
+ )
+ )
+
+
+class ReplayStorageCephForm(BaseReplayStorageForm):
+ ceph_bucket = forms.CharField(
+ max_length=128, label=_('Bucket'), required=False
+ )
+ ceph_access_key = forms.CharField(
+ max_length=128, label=_('Access key'), required=False,
+ widget=forms.PasswordInput
+ )
+ ceph_secret_key = forms.CharField(
+ max_length=128, label=_('Secret key'), required=False,
+ widget=forms.PasswordInput
+ )
+ ceph_endpoint = forms.CharField(
+ max_length=128, label=_('Endpoint'), required=False,
+ help_text=_(
+ """
+ S3: http://s3.{REGION_NAME}.amazonaws.com
+ S3(China): http://s3.{REGION_NAME}.amazonaws.com.cn
+ Example: http://s3.cn-north-1.amazonaws.com.cn
+ """
+ )
+ )
+
+
+class ReplayStorageSwiftForm(BaseReplayStorageForm):
+ swift_bucket = forms.CharField(
+ max_length=128, label=_('Bucket'), required=False
+ )
+ swift_access_key = forms.CharField(
+ max_length=128, label=_('Access key'), required=False,
+ widget=forms.PasswordInput
+ )
+ swift_secret_key = forms.CharField(
+ max_length=128, label=_('Secret key'), required=False,
+ widget=forms.PasswordInput
+ )
+ swift_region = forms.CharField(
+ max_length=128, label=_('Region'), required=False,
+ )
+ swift_endpoint = forms.CharField(
+ max_length=128, label=_('Endpoint'), required=False,
+ )
+
+
+class CommandStorageTypeESForm(BaseCommandStorageForm):
+ es_hosts = forms.CharField(
+ max_length=128, label=_('Hosts'), required=True,
+ help_text=_(
+ """
+ Tips: If there are multiple hosts, separate them with a comma (,)
+
+ eg: http://www.jumpserver.a.com,http://www.jumpserver.b.com
+ """
+ )
+ )
+ es_index = forms.CharField(
+ max_length=128, label=_('Index'), required=True
+ )
+ es_doc_type = forms.CharField(
+ max_length=128, label=_('Doc type'), required=True
+ )
diff --git a/apps/terminal/forms.py b/apps/terminal/forms/terminal.py
similarity index 69%
rename from apps/terminal/forms.py
rename to apps/terminal/forms/terminal.py
index b2f81fbb5..6051532d9 100644
--- a/apps/terminal/forms.py
+++ b/apps/terminal/forms/terminal.py
@@ -1,24 +1,22 @@
-# ~*~ coding: utf-8 ~*~
+# coding: utf-8
#
+__all__ = ['TerminalForm']
+
from django import forms
from django.utils.translation import ugettext_lazy as _
-from .models import Terminal
+from ..models import Terminal, ReplayStorage, CommandStorage
def get_all_command_storage():
- from common import utils
- command_storage = utils.get_command_storage_setting()
- for k, v in command_storage.items():
- yield (k, k)
+ for c in CommandStorage.objects.all():
+ yield (c.name, c.name)
def get_all_replay_storage():
- from common import utils
- replay_storage = utils.get_replay_storage_setting()
- for k, v in replay_storage.items():
- yield (k, k)
+ for r in ReplayStorage.objects.all():
+ yield (r.name, r.name)
class TerminalForm(forms.ModelForm):
diff --git a/apps/terminal/migrations/0016_commandstorage_replaystorage.py b/apps/terminal/migrations/0016_commandstorage_replaystorage.py
new file mode 100644
index 000000000..108112fa4
--- /dev/null
+++ b/apps/terminal/migrations/0016_commandstorage_replaystorage.py
@@ -0,0 +1,47 @@
+# Generated by Django 2.2.5 on 2019-11-22 10:07
+
+import common.fields.model
+from django.db import migrations, models
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('terminal', '0015_auto_20190923_1529'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='CommandStorage',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
+ ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
+ ('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')),
+ ('name', models.CharField(max_length=32, unique=True, verbose_name='Name')),
+ ('type', models.CharField(choices=[('null', 'Null'), ('server', 'Server'), ('es', 'Elasticsearch')], default='server', max_length=16, verbose_name='Type')),
+ ('meta', common.fields.model.EncryptJsonDictTextField(default={})),
+ ('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.CreateModel(
+ name='ReplayStorage',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
+ ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
+ ('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')),
+ ('name', models.CharField(max_length=32, unique=True, verbose_name='Name')),
+ ('type', models.CharField(choices=[('null', 'Null'), ('server', 'Server'), ('s3', 'S3'), ('ceph', 'Ceph'), ('swift', 'Swift'), ('oss', 'OSS'), ('azure', 'Azure')], default='server', max_length=16, verbose_name='Type')),
+ ('meta', common.fields.model.EncryptJsonDictTextField(default={})),
+ ('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ ]
diff --git a/apps/terminal/migrations/0017_auto_20191125_0931.py b/apps/terminal/migrations/0017_auto_20191125_0931.py
new file mode 100644
index 000000000..c555e6db6
--- /dev/null
+++ b/apps/terminal/migrations/0017_auto_20191125_0931.py
@@ -0,0 +1,85 @@
+# Generated by Django 2.2.5 on 2019-11-25 01:31
+
+from django.db import migrations
+
+
+def get_storage_data(s):
+ from common.utils import signer
+ import json
+ value = s.value
+ encrypted = s.encrypted
+ if encrypted:
+ value = signer.unsign(value)
+ try:
+ value = json.loads(value)
+ except:
+ value = {}
+ return value
+
+
+def get_setting(apps, schema_editor, key):
+ model = apps.get_model('settings', 'Setting')
+ db_alias = schema_editor.connection.alias
+ setting = model.objects.using(db_alias).filter(name=key)
+ if not setting:
+ return
+ return setting[0]
+
+
+def init_storage_data(model):
+ model.objects.update_or_create(
+ name='null', type='null',
+ defaults={
+ 'name': 'null', 'type': 'null',
+ 'comment': "Do not save"
+ }
+ )
+ model.objects.update_or_create(
+ name='default', type='server',
+ defaults={
+ 'name': 'default', 'type': 'server',
+ 'comment': "Store locally"
+ }
+ )
+
+
+def migrate_command_storage(apps, schema_editor):
+ model = apps.get_model("terminal", "CommandStorage")
+ init_storage_data(model)
+
+ setting = get_setting(apps, schema_editor, "TERMINAL_COMMAND_STORAGE")
+ if not setting:
+ return
+ values = get_storage_data(setting)
+ for name, meta in values.items():
+ tp = meta.pop("TYPE")
+ if not tp or name in ['default', 'null']:
+ continue
+ model.objects.create(name=name, type=tp, meta=meta)
+
+
+def migrate_replay_storage(apps, schema_editor):
+ model = apps.get_model("terminal", "ReplayStorage")
+ init_storage_data(model)
+
+ setting = get_setting(apps, schema_editor, "TERMINAL_REPLAY_STORAGE")
+ if not setting:
+ return
+ values = get_storage_data(setting)
+ for name, meta in values.items():
+ tp = meta.pop("TYPE", None)
+ if not tp or name in ['default', 'null']:
+ continue
+ model.objects.create(name=name, type=tp, meta=meta)
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('terminal', '0016_commandstorage_replaystorage'),
+ ]
+
+ operations = [
+ migrations.RunPython(migrate_command_storage),
+ migrations.RunPython(migrate_replay_storage),
+ ]
diff --git a/apps/terminal/migrations/0018_auto_20191202_1010.py b/apps/terminal/migrations/0018_auto_20191202_1010.py
new file mode 100644
index 000000000..d3dd8bc4c
--- /dev/null
+++ b/apps/terminal/migrations/0018_auto_20191202_1010.py
@@ -0,0 +1,67 @@
+# Generated by Django 2.2.7 on 2019-12-02 02:10
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('terminal', '0017_auto_20191125_0931'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='session',
+ name='date_last_active',
+ ),
+ migrations.AlterField(
+ model_name='session',
+ name='remote_addr',
+ field=models.CharField(blank=True, max_length=128, null=True,
+ verbose_name='Remote addr'),
+ ),
+ migrations.AddField(
+ model_name='session',
+ name='asset_id',
+ field=models.CharField(blank=True, db_index=True, default='',
+ max_length=36),
+ ),
+ migrations.AddField(
+ model_name='session',
+ name='system_user_id',
+ field=models.CharField(blank=True, db_index=True, default='',
+ max_length=36),
+ ),
+ migrations.AddField(
+ model_name='session',
+ name='user_id',
+ field=models.CharField(blank=True, db_index=True, default='',
+ max_length=36),
+ ),
+ migrations.AlterField(
+ model_name='session',
+ name='asset',
+ field=models.CharField(db_index=True, max_length=1024,
+ verbose_name='Asset'),
+ ),
+ migrations.AlterField(
+ model_name='session',
+ name='protocol',
+ field=models.CharField(
+ choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('vnc', 'vnc'),
+ ('telnet', 'telnet')], db_index=True, default='ssh',
+ max_length=8),
+ ),
+ migrations.AlterField(
+ model_name='session',
+ name='system_user',
+ field=models.CharField(db_index=True, max_length=128,
+ verbose_name='System user'),
+ ),
+ migrations.AlterField(
+ model_name='session',
+ name='user',
+ field=models.CharField(db_index=True, max_length=128,
+ verbose_name='User'),
+ ),
+ ]
diff --git a/apps/terminal/migrations/0019_auto_20191206_1000.py b/apps/terminal/migrations/0019_auto_20191206_1000.py
new file mode 100644
index 000000000..069920085
--- /dev/null
+++ b/apps/terminal/migrations/0019_auto_20191206_1000.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.7 on 2019-12-06 02:00
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('terminal', '0018_auto_20191202_1010'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='replaystorage',
+ name='type',
+ field=models.CharField(choices=[('null', 'Null'), ('server', 'Server'), ('s3', 'S3'), ('ceph', 'Ceph'), ('swift', 'Swift'), ('oss', 'OSS'), ('azure', 'Azure')], default='server', max_length=16, verbose_name='Type'),
+ ),
+ ]
diff --git a/apps/terminal/migrations/0020_auto_20191218_1721.py b/apps/terminal/migrations/0020_auto_20191218_1721.py
new file mode 100644
index 000000000..d9c6bfa5b
--- /dev/null
+++ b/apps/terminal/migrations/0020_auto_20191218_1721.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.1.11 on 2019-12-18 09:21
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('terminal', '0019_auto_20191206_1000'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='session',
+ name='protocol',
+ field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('vnc', 'vnc'), ('telnet', 'telnet'), ('mysql', 'mysql')], db_index=True, default='ssh', max_length=8),
+ ),
+ ]
diff --git a/apps/terminal/models.py b/apps/terminal/models.py
index f7f2994ed..372544510 100644
--- a/apps/terminal/models.py
+++ b/apps/terminal/models.py
@@ -2,6 +2,7 @@ from __future__ import unicode_literals
import os
import uuid
+import jms_storage
from django.db import models
from django.db.models.signals import post_save
@@ -13,9 +14,11 @@ from django.core.cache import cache
from users.models import User
from orgs.mixins.models import OrgModelMixin
-from common.utils import get_command_storage_setting, get_replay_storage_setting
+from common.mixins import CommonModelMixin
+from common.fields.model import EncryptJsonDictTextField
from .backends import get_multi_command_storage
from .backends.command.models import AbstractSessionCommand
+from . import const
class Terminal(models.Model):
@@ -55,21 +58,37 @@ class Terminal(models.Model):
self.user.is_active = active
self.user.save()
- def get_command_storage_setting(self):
- storage_all = get_command_storage_setting()
- if self.command_storage in storage_all:
- storage = storage_all.get(self.command_storage)
+ def get_command_storage(self):
+ storage = CommandStorage.objects.filter(name=self.command_storage).first()
+ return storage
+
+ def get_command_storage_config(self):
+ s = self.get_command_storage()
+ if s:
+ config = s.config
else:
- storage = storage_all.get('default')
- return {"TERMINAL_COMMAND_STORAGE": storage}
+ config = settings.DEFAULT_TERMINAL_COMMAND_STORAGE
+ return config
+
+ def get_command_storage_setting(self):
+ config = self.get_command_storage_config()
+ return {"TERMINAL_COMMAND_STORAGE": config}
+
+ def get_replay_storage(self):
+ storage = ReplayStorage.objects.filter(name=self.replay_storage).first()
+ return storage
+
+ def get_replay_storage_config(self):
+ s = self.get_replay_storage()
+ if s:
+ config = s.config
+ else:
+ config = settings.DEFAULT_TERMINAL_REPLAY_STORAGE
+ return config
def get_replay_storage_setting(self):
- storage_all = get_replay_storage_setting()
- if self.replay_storage in storage_all:
- storage = storage_all.get(self.replay_storage)
- else:
- storage = storage_all.get('default')
- return {"TERMINAL_REPLAY_STORAGE": storage}
+ config = self.get_replay_storage_config()
+ return {"TERMINAL_REPLAY_STORAGE": config}
@property
def config(self):
@@ -150,20 +169,23 @@ class Session(OrgModelMixin):
('rdp', 'rdp'),
('vnc', 'vnc'),
('telnet', 'telnet'),
+ ('mysql', 'mysql'),
)
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
- user = models.CharField(max_length=128, verbose_name=_("User"))
- asset = models.CharField(max_length=1024, verbose_name=_("Asset"))
- system_user = models.CharField(max_length=128, verbose_name=_("System user"))
+ user = models.CharField(max_length=128, verbose_name=_("User"), db_index=True)
+ user_id = models.CharField(blank=True, default='', max_length=36, db_index=True)
+ asset = models.CharField(max_length=1024, verbose_name=_("Asset"), db_index=True)
+ asset_id = models.CharField(blank=True, default='', max_length=36, db_index=True)
+ system_user = models.CharField(max_length=128, verbose_name=_("System user"), db_index=True)
+ system_user_id = models.CharField(blank=True, default='', max_length=36, db_index=True)
login_from = models.CharField(max_length=2, choices=LOGIN_FROM_CHOICES, default="ST")
- remote_addr = models.CharField(max_length=15, verbose_name=_("Remote addr"), blank=True, null=True)
+ remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True)
is_finished = models.BooleanField(default=False)
has_replay = models.BooleanField(default=False, verbose_name=_("Replay"))
has_command = models.BooleanField(default=False, verbose_name=_("Command"))
terminal = models.ForeignKey(Terminal, null=True, on_delete=models.SET_NULL)
- protocol = models.CharField(choices=PROTOCOL_CHOICES, default='ssh', max_length=8)
- date_last_active = models.DateTimeField(verbose_name=_("Date last active"), default=timezone.now)
+ protocol = models.CharField(choices=PROTOCOL_CHOICES, default='ssh', max_length=8, db_index=True)
date_start = models.DateTimeField(verbose_name=_("Date start"), db_index=True, default=timezone.now)
date_end = models.DateTimeField(verbose_name=_("Date end"), null=True)
@@ -229,7 +251,7 @@ class Session(OrgModelMixin):
return cls.objects.filter(is_finished=False)
def is_active(self):
- if self.protocol in ['ssh', 'telnet']:
+ if self.protocol in ['ssh', 'telnet', 'rdp', 'mysql']:
key = self.ACTIVE_CACHE_KEY_PREFIX.format(self.id)
return bool(cache.get(key))
return True
@@ -282,3 +304,89 @@ class Command(AbstractSessionCommand):
class Meta:
db_table = "terminal_command"
ordering = ('-timestamp',)
+
+
+class CommandStorage(CommonModelMixin):
+ TYPE_CHOICES = const.COMMAND_STORAGE_TYPE_CHOICES
+ 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)
+ type = models.CharField(
+ max_length=16, choices=TYPE_CHOICES, verbose_name=_('Type'),
+ default=TYPE_SERVER
+ )
+ meta = EncryptJsonDictTextField(default={})
+ comment = models.TextField(
+ max_length=128, default='', blank=True, verbose_name=_('Comment')
+ )
+
+ def __str__(self):
+ return self.name
+
+ @property
+ def config(self):
+ config = self.meta
+ config.update({'TYPE': self.type})
+ return config
+
+ def in_defaults(self):
+ return self.type in self.TYPE_DEFAULTS
+
+ def is_valid(self):
+ if self.in_defaults():
+ return True
+ storage = jms_storage.get_log_storage(self.config)
+ return storage.ping()
+
+ def can_delete(self):
+ return not self.in_defaults()
+
+
+class ReplayStorage(CommonModelMixin):
+ TYPE_CHOICES = const.REPLAY_STORAGE_TYPE_CHOICES
+ 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)
+ type = models.CharField(
+ max_length=16, choices=TYPE_CHOICES, verbose_name=_('Type'),
+ default=TYPE_SERVER
+ )
+ meta = EncryptJsonDictTextField(default={})
+ comment = models.TextField(
+ max_length=128, default='', blank=True, verbose_name=_('Comment')
+ )
+
+ def __str__(self):
+ return self.name
+
+ def convert_type(self):
+ s3_type_list = [
+ const.REPLAY_STORAGE_TYPE_CEPH, const.REPLAY_STORAGE_TYPE_SWIFT
+ ]
+ tp = self.type
+ if tp in s3_type_list:
+ tp = const.REPLAY_STORAGE_TYPE_S3
+ return tp
+
+ @property
+ def config(self):
+ config = self.meta
+ config.update({'TYPE': self.convert_type()})
+ return config
+
+ def in_defaults(self):
+ return self.type in self.TYPE_DEFAULTS
+
+ def is_valid(self):
+ if self.in_defaults():
+ return True
+ storage = jms_storage.get_object_storage(self.config)
+ target = 'tests.py'
+ src = os.path.join(settings.BASE_DIR, 'common', target)
+ return storage.is_valid(src, target)
+
+ def can_delete(self):
+ return not self.in_defaults()
+
diff --git a/apps/terminal/serializers/__init__.py b/apps/terminal/serializers/__init__.py
index e198ec278..83e851bae 100644
--- a/apps/terminal/serializers/__init__.py
+++ b/apps/terminal/serializers/__init__.py
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
#
-from .v1 import *
+from .terminal import *
+from .session import *
+from .storage import *
diff --git a/apps/terminal/serializers/session.py b/apps/terminal/serializers/session.py
new file mode 100644
index 000000000..8d71b2a95
--- /dev/null
+++ b/apps/terminal/serializers/session.py
@@ -0,0 +1,24 @@
+from rest_framework import serializers
+
+from orgs.mixins.serializers import BulkOrgResourceModelSerializer
+from common.serializers import AdaptedBulkListSerializer
+from ..models import Session
+
+
+class SessionSerializer(BulkOrgResourceModelSerializer):
+ command_amount = serializers.IntegerField(read_only=True)
+ org_id = serializers.CharField(allow_blank=True)
+
+ class Meta:
+ model = Session
+ list_serializer_class = AdaptedBulkListSerializer
+ fields = [
+ "id", "user", "asset", "system_user", "login_from",
+ "login_from_display", "remote_addr", "is_finished",
+ "has_replay", "can_replay", "protocol", "date_start", "date_end",
+ "terminal", "command_amount",
+ ]
+
+
+class ReplaySerializer(serializers.Serializer):
+ file = serializers.FileField(allow_empty_file=True)
diff --git a/apps/terminal/serializers/storage.py b/apps/terminal/serializers/storage.py
new file mode 100644
index 000000000..ea988e597
--- /dev/null
+++ b/apps/terminal/serializers/storage.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+#
+import copy
+from rest_framework import serializers
+
+from common.fields.serializer import CustomMetaDictField
+from ..models import ReplayStorage, CommandStorage
+from .. import const
+
+
+class ReplayStorageMetaDictField(CustomMetaDictField):
+ type_fields_map = const.REPLAY_STORAGE_TYPE_FIELDS_MAP
+ default_type = const.REPLAY_STORAGE_TYPE_SERVER
+ convert_key_remove_type_prefix = True
+ convert_key_to_upper = True
+
+
+class BaseStorageSerializerMixin:
+ type_fields_map = None
+
+ def process_meta(self, instance, validated_data):
+ new_meta = copy.deepcopy(validated_data.get('meta', {}))
+ tp = validated_data.get('type', '')
+
+ if tp != instance.type:
+ return new_meta
+
+ old_meta = instance.meta
+ fields = self.type_fields_map.get(instance.type, [])
+ for field in fields:
+ if not field.get('write_only', False):
+ continue
+ field_name = field['name']
+ new_value = new_meta.get(field_name, '')
+ old_value = old_meta.get(field_name, '')
+ field_value = new_value if new_value else old_value
+ new_meta[field_name] = field_value
+
+ return new_meta
+
+ def update(self, instance, validated_data):
+ meta = self.process_meta(instance, validated_data)
+ validated_data['meta'] = meta
+ return super().update(instance, validated_data)
+
+
+class ReplayStorageSerializer(BaseStorageSerializerMixin,
+ serializers.ModelSerializer):
+
+ meta = ReplayStorageMetaDictField()
+
+ type_fields_map = const.REPLAY_STORAGE_TYPE_FIELDS_MAP
+
+ class Meta:
+ model = ReplayStorage
+ fields = ['id', 'name', 'type', 'meta', 'comment']
+
+
+class CommandStorageMetaDictField(CustomMetaDictField):
+ type_fields_map = const.COMMAND_STORAGE_TYPE_FIELDS_MAP
+ default_type = const.COMMAND_STORAGE_TYPE_SERVER
+ convert_key_remove_type_prefix = True
+ convert_key_to_upper = True
+
+
+class CommandStorageSerializer(BaseStorageSerializerMixin,
+ serializers.ModelSerializer):
+
+ meta = CommandStorageMetaDictField()
+
+ type_fields_map = const.COMMAND_STORAGE_TYPE_FIELDS_MAP
+
+ class Meta:
+ model = CommandStorage
+ fields = ['id', 'name', 'type', 'meta', 'comment']
diff --git a/apps/terminal/serializers/v1.py b/apps/terminal/serializers/terminal.py
similarity index 55%
rename from apps/terminal/serializers/v1.py
rename to apps/terminal/serializers/terminal.py
index 96560e374..1df4b40e6 100644
--- a/apps/terminal/serializers/v1.py
+++ b/apps/terminal/serializers/terminal.py
@@ -1,11 +1,10 @@
-# -*- coding: utf-8 -*-
-#
from rest_framework import serializers
-from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.mixins import BulkSerializerMixin
from common.serializers import AdaptedBulkListSerializer
-from ..models import Terminal, Status, Session, Task
+from ..models import (
+ Terminal, Status, Session, Task
+)
class TerminalSerializer(serializers.ModelSerializer):
@@ -25,21 +24,6 @@ class TerminalSerializer(serializers.ModelSerializer):
return Session.objects.filter(terminal=obj, is_finished=False).count()
-class SessionSerializer(BulkOrgResourceModelSerializer):
- command_amount = serializers.IntegerField(read_only=True)
- org_id = serializers.CharField(allow_blank=True)
-
- class Meta:
- model = Session
- list_serializer_class = AdaptedBulkListSerializer
- fields = [
- "id", "user", "asset", "system_user", "login_from",
- "login_from_display", "remote_addr", "is_finished",
- "has_replay", "can_replay", "protocol", "date_start", "date_end",
- "terminal", "command_amount",
- ]
-
-
class StatusSerializer(serializers.ModelSerializer):
class Meta:
fields = ['id', 'terminal']
@@ -52,9 +36,3 @@ class TaskSerializer(BulkSerializerMixin, serializers.ModelSerializer):
fields = '__all__'
model = Task
list_serializer_class = AdaptedBulkListSerializer
-
-
-class ReplaySerializer(serializers.Serializer):
- file = serializers.FileField(allow_empty_file=True)
-
-
diff --git a/apps/terminal/templates/terminal/base_storage_create_update.html b/apps/terminal/templates/terminal/base_storage_create_update.html
new file mode 100644
index 000000000..e432ecfda
--- /dev/null
+++ b/apps/terminal/templates/terminal/base_storage_create_update.html
@@ -0,0 +1,59 @@
+{% extends '_base_create_update.html' %}
+{% load static %}
+{% load bootstrap3 %}
+{% load i18n %}
+
+{% block form %}
+
+ {% bootstrap_form form layout="horizontal"%}
+
+
+
+
+{% endblock %}
+
+{% block custom_foot_js %}
+
+{% endblock %}
diff --git a/apps/terminal/templates/terminal/base_storage_list.html b/apps/terminal/templates/terminal/base_storage_list.html
new file mode 100644
index 000000000..40e93664b
--- /dev/null
+++ b/apps/terminal/templates/terminal/base_storage_list.html
@@ -0,0 +1,130 @@
+{% extends 'base.html' %}
+{% load i18n static %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+
+ {% block create_storage_info %}{% endblock %}
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
+{% block content_bottom_left %}{% endblock %}
+{% block custom_foot_js %}
+
+{% endblock %}
+
diff --git a/apps/terminal/templates/terminal/command_list.html b/apps/terminal/templates/terminal/command_list.html
index 1fc6f1d76..5e2dcddc9 100644
--- a/apps/terminal/templates/terminal/command_list.html
+++ b/apps/terminal/templates/terminal/command_list.html
@@ -168,7 +168,7 @@ $(document).ready(function () {
function format(d) {
- var output = $("
");
+ var output = $("
");
output.append('$ ', d.input);
output.append('\r\n\r\n');
diff --git a/apps/terminal/templates/terminal/command_storage_create_update.html b/apps/terminal/templates/terminal/command_storage_create_update.html
new file mode 100644
index 000000000..8abb976ae
--- /dev/null
+++ b/apps/terminal/templates/terminal/command_storage_create_update.html
@@ -0,0 +1,30 @@
+{% extends 'terminal/base_storage_create_update.html' %}
+{% load i18n static %}
+
+{% block custom_foot_js %}
+{{ block.super }}
+
+
+{% endblock %}
diff --git a/apps/terminal/templates/terminal/command_storage_list.html b/apps/terminal/templates/terminal/command_storage_list.html
new file mode 100644
index 000000000..724425cca
--- /dev/null
+++ b/apps/terminal/templates/terminal/command_storage_list.html
@@ -0,0 +1,15 @@
+{% extends 'terminal/base_storage_list.html' %}
+{% load i18n static %}
+
+{% block create_storage_url %}{% url "terminal:command-storage-create" %}{% endblock%}
+{% block create_storage_info %}{% trans 'Create command storage' %}{% endblock %}
+{% block custom_foot_js %}
+{{ block.super }}
+
+{% endblock %}
+
diff --git a/apps/terminal/templates/terminal/replay_storage_create_update.html b/apps/terminal/templates/terminal/replay_storage_create_update.html
new file mode 100644
index 000000000..878301d93
--- /dev/null
+++ b/apps/terminal/templates/terminal/replay_storage_create_update.html
@@ -0,0 +1,25 @@
+{% extends 'terminal/base_storage_create_update.html' %}
+{% load i18n static %}
+
+{% block custom_foot_js %}
+{{ block.super }}
+
+
+{% endblock %}
diff --git a/apps/terminal/templates/terminal/replay_storage_list.html b/apps/terminal/templates/terminal/replay_storage_list.html
new file mode 100644
index 000000000..786f4c844
--- /dev/null
+++ b/apps/terminal/templates/terminal/replay_storage_list.html
@@ -0,0 +1,14 @@
+{% extends 'terminal/base_storage_list.html' %}
+{% load i18n static %}
+
+{% block create_storage_url %}{% url "terminal:replay-storage-create" %}{% endblock%}
+{% block create_storage_info %}{% trans 'Create replay storage' %}{% endblock %}
+{% block custom_foot_js %}
+{{ block.super }}
+
+{% endblock %}
diff --git a/apps/terminal/templates/terminal/session_detail.html b/apps/terminal/templates/terminal/session_detail.html
index 1c96e3ee7..ea5ba9b00 100644
--- a/apps/terminal/templates/terminal/session_detail.html
+++ b/apps/terminal/templates/terminal/session_detail.html
@@ -55,7 +55,7 @@
{{ forloop.counter }}
{{ command.input | truncatechars:40 }}
-
+
$ {{ command.input }}
{{ command.output }}
@@ -136,7 +136,13 @@ $ {{ command.input }}
}, 300)
}
var the_url = "{% url 'api-terminal:tasks-list' %}";
- requestApi({url: the_url, method: 'POST', body: JSON.stringify(data), success: success, success_message: 'Terminate success'});
+ requestApi({
+ url: the_url,
+ method: 'POST',
+ body: JSON.stringify(data),
+ success: success,
+ success_message: "{% trans 'Terminate success'%}"
+ });
}
$(document).ready(function () {
$('.footable').footable();
diff --git a/apps/terminal/templates/terminal/session_list.html b/apps/terminal/templates/terminal/session_list.html
index 1393bd241..965b8f556 100644
--- a/apps/terminal/templates/terminal/session_list.html
+++ b/apps/terminal/templates/terminal/session_list.html
@@ -5,8 +5,6 @@
{% load common_tags %}
{% block custom_head_css_js %}
-
-
{% endblock %}
{% block content_left_head %}
diff --git a/apps/terminal/templates/terminal/terminal_list.html b/apps/terminal/templates/terminal/terminal_list.html
index 53325694b..f77949bfa 100644
--- a/apps/terminal/templates/terminal/terminal_list.html
+++ b/apps/terminal/templates/terminal/terminal_list.html
@@ -18,6 +18,7 @@
{% block table_search %}{% endblock %}
{% block table_container %}
+
diff --git a/apps/terminal/templates/terminal/terminal_update.html b/apps/terminal/templates/terminal/terminal_update.html
index f9554fda4..e140cd678 100644
--- a/apps/terminal/templates/terminal/terminal_update.html
+++ b/apps/terminal/templates/terminal/terminal_update.html
@@ -3,8 +3,6 @@
{% load static %}
{% load bootstrap3 %}
{% block custom_head_css_js %}
-
-
{% endblock %}
diff --git a/apps/terminal/urls/api_urls.py b/apps/terminal/urls/api_urls.py
index 96db7d31f..8c19fa685 100644
--- a/apps/terminal/urls/api_urls.py
+++ b/apps/terminal/urls/api_urls.py
@@ -18,6 +18,8 @@ router.register(r'terminals', api.TerminalViewSet, 'terminal')
router.register(r'tasks', api.TaskViewSet, 'tasks')
router.register(r'commands', api.CommandViewSet, 'command')
router.register(r'status', api.StatusViewSet, 'status')
+router.register(r'replay-storages', api.ReplayStorageViewSet, 'replay-storage')
+router.register(r'command-storages', api.CommandStorageViewSet, 'command-storage')
urlpatterns = [
path('sessions//replay/',
@@ -27,7 +29,9 @@ urlpatterns = [
path('terminals//access-key/', api.TerminalTokenApi.as_view(),
name='terminal-access-key'),
path('terminals/config/', api.TerminalConfig.as_view(), name='terminal-config'),
- path('commands/export/', api.CommandExportApi.as_view(), name="command-export")
+ path('commands/export/', api.CommandExportApi.as_view(), name="command-export"),
+ path('replay-storages//test-connective/', api.ReplayStorageTestConnectiveApi.as_view(), name='replay-storage-test-connective'),
+ path('command-storages//test-connective/', api.CommandStorageTestConnectiveApi.as_view(), name='command-storage-test-connective')
# v2: get session's replay
# path('v2/sessions//replay/',
# api.SessionReplayV2ViewSet.as_view({'get': 'retrieve'}),
diff --git a/apps/terminal/urls/views_urls.py b/apps/terminal/urls/views_urls.py
index 61db49c45..5caec1e75 100644
--- a/apps/terminal/urls/views_urls.py
+++ b/apps/terminal/urls/views_urls.py
@@ -26,4 +26,14 @@ urlpatterns = [
# Command view
path('command/', views.CommandListView.as_view(), name='command-list'),
+ # replay-storage
+ path('terminal/replay-storage/', views.ReplayStorageListView.as_view(), name='replay-storage-list'),
+ path('terminal/replay-storage/create/', views.ReplayStorageCreateView.as_view(), name='replay-storage-create'),
+ path('terminal/replay-storage//update/', views.ReplayStorageUpdateView.as_view(), name='replay-storage-update'),
+
+ # command-storage
+ path('terminal/command-storage/', views.CommandStorageListView.as_view(), name='command-storage-list'),
+ path('terminal/command-storage/create/', views.CommandStorageCreateView.as_view(), name='command-storage-create'),
+ path('terminal/command-storage//update/', views.CommandStorageUpdateView.as_view(), name='command-storage-update'),
+
]
diff --git a/apps/terminal/views/__init__.py b/apps/terminal/views/__init__.py
index 8267a40bc..a63c3cf9a 100644
--- a/apps/terminal/views/__init__.py
+++ b/apps/terminal/views/__init__.py
@@ -3,3 +3,7 @@
from .terminal import *
from .session import *
from .command import *
+from .storage import *
+
+# from .replay_storage import *
+# from .command_storage import *
diff --git a/apps/terminal/views/storage.py b/apps/terminal/views/storage.py
new file mode 100644
index 000000000..778cdd489
--- /dev/null
+++ b/apps/terminal/views/storage.py
@@ -0,0 +1,181 @@
+# coding: utf-8
+#
+
+from django.http import Http404
+from django.views.generic import TemplateView
+from django.views.generic.edit import CreateView, UpdateView
+from django.utils.translation import ugettext as _
+
+from common.permissions import PermissionsMixin, IsSuperUser
+from terminal.models import ReplayStorage, CommandStorage
+from .. import forms, const
+
+
+__all__ = [
+ 'ReplayStorageListView', 'ReplayStorageCreateView',
+ 'ReplayStorageUpdateView', 'CommandStorageListView',
+ 'CommandStorageCreateView', 'CommandStorageUpdateView'
+]
+
+
+class ReplayStorageListView(PermissionsMixin, TemplateView):
+ template_name = 'terminal/replay_storage_list.html'
+ permission_classes = [IsSuperUser]
+
+ def get_context_data(self, **kwargs):
+ context = {
+ 'app': _('Terminal'),
+ 'action': _('Replay storage list'),
+ 'is_replay': True,
+ 'type_choices': const.REPLAY_STORAGE_TYPE_CHOICES_EXTENDS,
+ }
+ kwargs.update(context)
+ return super().get_context_data(**kwargs)
+
+
+class CommandStorageListView(PermissionsMixin, TemplateView):
+ template_name = 'terminal/command_storage_list.html'
+ permission_classes = [IsSuperUser]
+
+ def get_context_data(self, **kwargs):
+ context = {
+ 'app': _('Terminal'),
+ 'action': _('Command storage list'),
+ 'type_choices': const.COMMAND_STORAGE_TYPE_CHOICES_EXTENDS,
+ 'is_command': True,
+ }
+ kwargs.update(context)
+ return super().get_context_data(**kwargs)
+
+
+class BaseStorageCreateUpdateViewMixin:
+ permission_classes = [IsSuperUser]
+ default_type = None
+ form_class = None
+ form_class_choices = {}
+
+ def get_initial(self):
+ return {'type': self.get_type()}
+
+ def get_type(self):
+ return self.default_type
+
+ def get_form_class(self):
+ tp = self.get_type()
+ form_class = self.form_class_choices.get(tp)
+ if not form_class:
+ raise Http404()
+ return form_class
+
+
+class ReplayStorageCreateUpdateViewMixin(BaseStorageCreateUpdateViewMixin):
+ model = ReplayStorage
+ default_type = const.REPLAY_STORAGE_TYPE_S3
+ form_class = forms.ReplayStorageS3Form
+ form_class_choices = {
+ const.REPLAY_STORAGE_TYPE_S3: forms.ReplayStorageS3Form,
+ const.REPLAY_STORAGE_TYPE_CEPH: forms.ReplayStorageCephForm,
+ const.REPLAY_STORAGE_TYPE_SWIFT: forms.ReplayStorageSwiftForm,
+ const.REPLAY_STORAGE_TYPE_OSS: forms.ReplayStorageOSSForm,
+ const.REPLAY_STORAGE_TYPE_AZURE: forms.ReplayStorageAzureForm
+ }
+
+
+class ReplayStorageCreateView(ReplayStorageCreateUpdateViewMixin,
+ PermissionsMixin, CreateView):
+ template_name = 'terminal/replay_storage_create_update.html'
+
+ def get_type(self):
+ tp = self.request.GET.get("type")
+ if tp:
+ return tp.lower()
+ return super().get_type()
+
+ def get_context_data(self, **kwargs):
+ context = {
+ 'app': _('Terminal'),
+ 'action': _('Create replay storage'),
+ 'api_action': 'create'
+ }
+ kwargs.update(context)
+ return super().get_context_data(**kwargs)
+
+
+class ReplayStorageUpdateView(ReplayStorageCreateUpdateViewMixin,
+ PermissionsMixin, UpdateView):
+ template_name = 'terminal/replay_storage_create_update.html'
+
+ def get_initial(self):
+ initial_data = super().get_initial()
+ for k, v in self.object.meta.items():
+ _k = "{}_{}".format(self.object.type, k.lower())
+ initial_data[_k] = v
+ return initial_data
+
+ def get_type(self):
+ return self.object.type
+
+ def get_context_data(self, **kwargs):
+ context = {
+ 'app': _('Terminal'),
+ 'action': _('Update replay storage'),
+ 'api_action': 'update'
+ }
+ kwargs.update(context)
+ return super().get_context_data(**kwargs)
+
+
+class CommandStorageCreateUpdateViewMixin(BaseStorageCreateUpdateViewMixin):
+ model = CommandStorage
+ default_type = const.COMMAND_STORAGE_TYPE_ES
+ form_class = forms.CommandStorageTypeESForm
+ form_class_choices = {
+ const.COMMAND_STORAGE_TYPE_ES: forms.CommandStorageTypeESForm
+ }
+
+
+class CommandStorageCreateView(CommandStorageCreateUpdateViewMixin,
+ PermissionsMixin, CreateView):
+ template_name = 'terminal/command_storage_create_update.html'
+
+ def get_type(self):
+ tp = self.request.GET.get("type")
+ if tp:
+ return tp.lower()
+ return super().get_type()
+
+ def get_context_data(self, **kwargs):
+ context = {
+ 'app': _('Terminal'),
+ 'action': _('Create command storage'),
+ 'api_action': 'create'
+ }
+ kwargs.update(context)
+ return super().get_context_data(**kwargs)
+
+
+class CommandStorageUpdateView(CommandStorageCreateUpdateViewMixin,
+ PermissionsMixin, UpdateView):
+ template_name = 'terminal/command_storage_create_update.html'
+
+ def get_initial(self):
+ initial_data = super().get_initial()
+ for k, v in self.object.meta.items():
+ _k = "{}_{}".format(self.object.type, k.lower())
+ if k == 'HOSTS':
+ v = ','.join(v)
+ initial_data[_k] = v
+ return initial_data
+
+ def get_type(self):
+ return self.object.type
+
+ def get_context_data(self, **kwargs):
+ context = {
+ 'app': _('Terminal'),
+ 'action': _('Update command storage'),
+ 'api_action': 'update'
+ }
+ kwargs.update(context)
+ return super().get_context_data(**kwargs)
+
diff --git a/apps/tickets/api/ticket.py b/apps/tickets/api/ticket.py
index 1706c38a7..5e3d90701 100644
--- a/apps/tickets/api/ticket.py
+++ b/apps/tickets/api/ticket.py
@@ -13,7 +13,7 @@ class TicketViewSet(mixins.TicketMixin, viewsets.ModelViewSet):
serializer_class = serializers.TicketSerializer
queryset = models.Ticket.objects.all()
permission_classes = (IsValidUser,)
- filter_fields = ['status', 'title', 'action']
+ filter_fields = ['status', 'title', 'action', 'user_display']
search_fields = ['user_display', 'title']
diff --git a/apps/tickets/serializers/ticket.py b/apps/tickets/serializers/ticket.py
index f148c6375..e6ca13a2b 100644
--- a/apps/tickets/serializers/ticket.py
+++ b/apps/tickets/serializers/ticket.py
@@ -32,8 +32,6 @@ class TicketSerializer(serializers.ModelSerializer):
if action and user not in instance.assignees.all():
error = {"action": "Only assignees can update"}
raise serializers.ValidationError(error)
- print(validated_data)
- print(instance.status)
if instance.status == instance.STATUS_CLOSED:
validated_data.pop('action')
instance = super().update(instance, validated_data)
diff --git a/apps/users/api/group.py b/apps/users/api/group.py
index eb00ea220..860ca36b4 100644
--- a/apps/users/api/group.py
+++ b/apps/users/api/group.py
@@ -1,35 +1,19 @@
# -*- coding: utf-8 -*-
#
-from ..serializers import (
- UserGroupSerializer,
- UserGroupListSerializer,
- UserGroupUpdateMemberSerializer,
-)
+from ..serializers import UserGroupSerializer
from ..models import UserGroup
from orgs.mixins.api import OrgBulkModelViewSet
-from orgs.mixins import generics
from common.permissions import IsOrgAdmin
-__all__ = ['UserGroupViewSet', 'UserGroupUpdateUserApi']
+__all__ = ['UserGroupViewSet']
class UserGroupViewSet(OrgBulkModelViewSet):
model = UserGroup
filter_fields = ("name",)
search_fields = filter_fields
+ permission_classes = (IsOrgAdmin,)
serializer_class = UserGroupSerializer
- permission_classes = (IsOrgAdmin,)
- def get_serializer_class(self):
- if self.action in ("list", 'retrieve') and \
- self.request.query_params.get("display"):
- return UserGroupListSerializer
- return self.serializer_class
-
-
-class UserGroupUpdateUserApi(generics.RetrieveUpdateAPIView):
- model = UserGroup
- serializer_class = UserGroupUpdateMemberSerializer
- permission_classes = (IsOrgAdmin,)
diff --git a/apps/users/api/relation.py b/apps/users/api/relation.py
index a579419d5..fbab92ee9 100644
--- a/apps/users/api/relation.py
+++ b/apps/users/api/relation.py
@@ -19,8 +19,8 @@ class UserUserGroupRelationViewSet(BulkModelViewSet):
def get_queryset(self):
queryset = User.groups.through.objects.all()\
- .annotate(user_name=F('user__name'))\
- .annotate(usergroup_name=F('usergroup__name'))
+ .annotate(user_display=F('user__name'))\
+ .annotate(usergroup_display=F('usergroup__name'))
return queryset
def allow_bulk_destroy(self, qs, filtered):
diff --git a/apps/users/api/user.py b/apps/users/api/user.py
index 3087658e2..98dcbd91c 100644
--- a/apps/users/api/user.py
+++ b/apps/users/api/user.py
@@ -24,7 +24,7 @@ from ..signals import post_user_create
logger = get_logger(__name__)
__all__ = [
- 'UserViewSet', 'UserChangePasswordApi', 'UserUpdateGroupApi',
+ 'UserViewSet', 'UserChangePasswordApi',
'UserResetPasswordApi', 'UserResetPKApi', 'UserUpdatePKApi',
'UserUnblockPKApi', 'UserProfileApi', 'UserResetOTPApi',
]
@@ -39,8 +39,10 @@ class UserQuerysetMixin:
class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet):
filter_fields = ('username', 'email', 'name', 'id')
search_fields = filter_fields
- serializer_class = serializers.UserSerializer
- serializer_display_class = serializers.UserDisplaySerializer
+ serializer_classes = {
+ 'default': serializers.UserSerializer,
+ 'display': serializers.UserDisplaySerializer
+ }
permission_classes = (IsOrgAdmin, CanUpdateDeleteUser)
def get_queryset(self):
@@ -67,6 +69,12 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet):
self.permission_classes = (IsSuperUser,)
return super().get_permissions()
+ def perform_destroy(self, instance):
+ if current_org.is_real():
+ instance.remove()
+ else:
+ return super().perform_destroy(instance)
+
def perform_bulk_destroy(self, objects):
for obj in objects:
self.check_object_permissions(self.request, obj)
@@ -93,11 +101,6 @@ class UserChangePasswordApi(UserQuerysetMixin, generics.RetrieveUpdateAPIView):
user.save()
-class UserUpdateGroupApi(UserQuerysetMixin, generics.RetrieveUpdateAPIView):
- serializer_class = serializers.UserUpdateGroupSerializer
- permission_classes = (IsOrgAdmin,)
-
-
class UserResetPasswordApi(UserQuerysetMixin, generics.UpdateAPIView):
queryset = User.objects.all()
serializer_class = serializers.UserSerializer
@@ -176,5 +179,4 @@ class UserResetOTPApi(UserQuerysetMixin, generics.RetrieveAPIView):
if user.mfa_enabled:
user.reset_mfa()
user.save()
- logout(request)
return Response({"msg": "success"})
diff --git a/apps/users/forms/__init__.py b/apps/users/forms/__init__.py
new file mode 100644
index 000000000..8b4d5d888
--- /dev/null
+++ b/apps/users/forms/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+#
+from .user import *
+from .group import *
+from .profile import *
diff --git a/apps/users/forms/group.py b/apps/users/forms/group.py
new file mode 100644
index 000000000..8d026a777
--- /dev/null
+++ b/apps/users/forms/group.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+#
+from django import forms
+from django.utils.translation import gettext_lazy as _
+
+from orgs.mixins.forms import OrgModelForm
+from ..models import User, UserGroup
+
+__all__ = ['UserGroupForm']
+
+
+class UserGroupForm(OrgModelForm):
+ users = forms.ModelMultipleChoiceField(
+ queryset=User.objects.none(),
+ label=_("User"),
+ widget=forms.SelectMultiple(
+ attrs={
+ 'class': 'users-select2',
+ 'data-placeholder': _('Select users')
+ }
+ ),
+ required=False,
+ )
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.set_fields_queryset()
+
+ def set_fields_queryset(self):
+ users_field = self.fields.get('users')
+ if self.instance:
+ users_field.initial = self.instance.users.all()
+ users_field.queryset = self.instance.users.all()
+ else:
+ users_field.queryset = User.objects.none()
+
+ def save(self, commit=True):
+ raise Exception("Save by restful api")
+
+ class Meta:
+ model = UserGroup
+ fields = [
+ 'name', 'users', 'comment',
+ ]
diff --git a/apps/users/forms/profile.py b/apps/users/forms/profile.py
new file mode 100644
index 000000000..bd1047733
--- /dev/null
+++ b/apps/users/forms/profile.py
@@ -0,0 +1,152 @@
+# -*- coding: utf-8 -*-
+#
+from django import forms
+from django.utils.translation import gettext_lazy as _
+from captcha.fields import CaptchaField
+
+from common.utils import validate_ssh_public_key
+from ..models import User
+
+
+__all__ = [
+ 'UserProfileForm', 'UserMFAForm', 'UserFirstLoginFinishForm',
+ 'UserPasswordForm', 'UserPublicKeyForm', 'FileForm',
+ 'UserTokenResetPasswordForm', 'UserForgotPasswordForm',
+]
+
+
+class UserProfileForm(forms.ModelForm):
+ username = forms.CharField(disabled=True, label=_("Username"))
+ name = forms.CharField(disabled=True, label=_("Name"))
+ email = forms.CharField(disabled=True)
+
+ class Meta:
+ model = User
+ fields = [
+ 'username', 'name', 'email',
+ 'wechat', 'phone',
+ ]
+
+
+UserProfileForm.verbose_name = _("Profile")
+
+
+class UserMFAForm(forms.ModelForm):
+
+ mfa_description = _(
+ 'When enabled, '
+ 'you will enter the MFA binding process the next time you log in. '
+ 'you can also directly bind in '
+ '"personal information -> quick modification -> change MFA Settings"!')
+
+ class Meta:
+ model = User
+ fields = ['mfa_level']
+ widgets = {'mfa_level': forms.RadioSelect()}
+ help_texts = {
+ 'mfa_level': _('* Enable MFA authentication '
+ 'to make the account more secure.'),
+ }
+
+
+UserMFAForm.verbose_name = _("MFA")
+
+
+class UserFirstLoginFinishForm(forms.Form):
+ finish_description = _(
+ 'In order to protect you and your company, '
+ 'please keep your account, '
+ 'password and key sensitive information properly. '
+ '(for example: setting complex password, enabling MFA authentication)'
+ )
+
+
+UserFirstLoginFinishForm.verbose_name = _("Finish")
+
+
+class UserTokenResetPasswordForm(forms.Form):
+ new_password = forms.CharField(
+ min_length=5, max_length=128,
+ widget=forms.PasswordInput,
+ label=_("New password")
+ )
+ confirm_password = forms.CharField(
+ min_length=5, max_length=128,
+ widget=forms.PasswordInput,
+ label=_("Confirm password")
+ )
+
+ def clean_confirm_password(self):
+ new_password = self.cleaned_data['new_password']
+ confirm_password = self.cleaned_data['confirm_password']
+
+ if new_password != confirm_password:
+ raise forms.ValidationError(_('Password does not match'))
+ return confirm_password
+
+
+class UserForgotPasswordForm(forms.Form):
+ email = forms.EmailField(label=_("Email"))
+ captcha = CaptchaField(label=_("Captcha"))
+
+
+class UserPasswordForm(UserTokenResetPasswordForm):
+ old_password = forms.CharField(
+ max_length=128, widget=forms.PasswordInput,
+ label=_("Old password")
+ )
+
+ def __init__(self, *args, **kwargs):
+ self.instance = kwargs.pop('instance')
+ super().__init__(*args, **kwargs)
+
+ def clean_old_password(self):
+ old_password = self.cleaned_data['old_password']
+ if not self.instance.check_password(old_password):
+ raise forms.ValidationError(_('Old password error'))
+ return old_password
+
+ def save(self):
+ password = self.cleaned_data['new_password']
+ self.instance.reset_password(new_password=password)
+ return self.instance
+
+
+class UserPublicKeyForm(forms.Form):
+ pubkey_description = _('Automatically configure and download the SSH key')
+ public_key = forms.CharField(
+ label=_('ssh public key'), max_length=5000, required=False,
+ widget=forms.Textarea(attrs={'placeholder': _('ssh-rsa AAAA...')}),
+ help_text=_('Paste your id_rsa.pub here.')
+ )
+
+ def __init__(self, *args, **kwargs):
+ if 'instance' in kwargs:
+ self.instance = kwargs.pop('instance')
+ else:
+ self.instance = None
+ super().__init__(*args, **kwargs)
+
+ def clean_public_key(self):
+ public_key = self.cleaned_data['public_key']
+ if self.instance.public_key and public_key == self.instance.public_key:
+ msg = _('Public key should not be the same as your old one.')
+ raise forms.ValidationError(msg)
+
+ if public_key and not validate_ssh_public_key(public_key):
+ raise forms.ValidationError(_('Not a valid ssh public key'))
+ return public_key
+
+ def save(self):
+ public_key = self.cleaned_data['public_key']
+ if public_key:
+ self.instance.public_key = public_key
+ self.instance.save()
+ return self.instance
+
+
+UserPublicKeyForm.verbose_name = _("Public key")
+
+
+class FileForm(forms.Form):
+ file = forms.FileField()
diff --git a/apps/users/forms.py b/apps/users/forms/user.py
similarity index 52%
rename from apps/users/forms.py
rename to apps/users/forms/user.py
index 6b360fb71..a58e1fef1 100644
--- a/apps/users/forms.py
+++ b/apps/users/forms/user.py
@@ -1,39 +1,19 @@
-# ~*~ coding: utf-8 ~*~
from django import forms
from django.utils.translation import gettext_lazy as _
-from django.conf import settings
from common.utils import validate_ssh_public_key
from orgs.mixins.forms import OrgModelForm
-from .models import User, UserGroup
-from .utils import check_password_rules, get_current_org_members
+from ..models import User
+from ..utils import (
+ check_password_rules, get_current_org_members, get_source_choices
+)
-class UserCheckPasswordForm(forms.Form):
- username = forms.CharField(label=_('Username'), max_length=100)
- password = forms.CharField(
- label=_('Password'), widget=forms.PasswordInput,
- max_length=128, strip=False
- )
-
-
-class UserCheckOtpCodeForm(forms.Form):
- otp_code = forms.CharField(label=_('MFA code'), max_length=6)
-
-
-def get_source_choices():
- choices_all = dict(User.SOURCE_CHOICES)
- choices = [
- (User.SOURCE_LOCAL, choices_all[User.SOURCE_LOCAL]),
- ]
- if settings.AUTH_LDAP:
- choices.append((User.SOURCE_LDAP, choices_all[User.SOURCE_LDAP]))
- if settings.AUTH_OPENID:
- choices.append((User.SOURCE_OPENID, choices_all[User.SOURCE_OPENID]))
- if settings.AUTH_RADIUS:
- choices.append((User.SOURCE_RADIUS, choices_all[User.SOURCE_RADIUS]))
- return choices
+__all__ = [
+ 'UserCreateForm', 'UserUpdateForm', 'UserBulkUpdateForm',
+ 'UserCheckOtpCodeForm', 'UserCheckPasswordForm'
+]
class UserCreateUpdateFormMixin(OrgModelForm):
@@ -157,131 +137,6 @@ class UserUpdateForm(UserCreateUpdateFormMixin):
pass
-class UserProfileForm(forms.ModelForm):
- username = forms.CharField(disabled=True, label=_("Username"))
- name = forms.CharField(disabled=True, label=_("Name"))
- email = forms.CharField(disabled=True)
-
- class Meta:
- model = User
- fields = [
- 'username', 'name', 'email',
- 'wechat', 'phone',
- ]
-
-
-UserProfileForm.verbose_name = _("Profile")
-
-
-class UserMFAForm(forms.ModelForm):
-
- mfa_description = _(
- 'When enabled, '
- 'you will enter the MFA binding process the next time you log in. '
- 'you can also directly bind in '
- '"personal information -> quick modification -> change MFA Settings"!')
-
- class Meta:
- model = User
- fields = ['mfa_level']
- widgets = {'mfa_level': forms.RadioSelect()}
- help_texts = {
- 'mfa_level': _('* Enable MFA authentication '
- 'to make the account more secure.'),
- }
-
-
-UserMFAForm.verbose_name = _("MFA")
-
-
-class UserFirstLoginFinishForm(forms.Form):
- finish_description = _(
- 'In order to protect you and your company, '
- 'please keep your account, '
- 'password and key sensitive information properly. '
- '(for example: setting complex password, enabling MFA authentication)'
- )
-
-
-UserFirstLoginFinishForm.verbose_name = _("Finish")
-
-
-class UserPasswordForm(forms.Form):
- old_password = forms.CharField(
- max_length=128, widget=forms.PasswordInput,
- label=_("Old password")
- )
- new_password = forms.CharField(
- min_length=5, max_length=128,
- widget=forms.PasswordInput,
- label=_("New password")
- )
- confirm_password = forms.CharField(
- min_length=5, max_length=128,
- widget=forms.PasswordInput,
- label=_("Confirm password")
- )
-
- def __init__(self, *args, **kwargs):
- self.instance = kwargs.pop('instance')
- super().__init__(*args, **kwargs)
-
- def clean_old_password(self):
- old_password = self.cleaned_data['old_password']
- if not self.instance.check_password(old_password):
- raise forms.ValidationError(_('Old password error'))
- return old_password
-
- def clean_confirm_password(self):
- new_password = self.cleaned_data['new_password']
- confirm_password = self.cleaned_data['confirm_password']
-
- if new_password != confirm_password:
- raise forms.ValidationError(_('Password does not match'))
- return confirm_password
-
- def save(self):
- password = self.cleaned_data['new_password']
- self.instance.reset_password(new_password=password)
- return self.instance
-
-
-class UserPublicKeyForm(forms.Form):
- pubkey_description = _('Automatically configure and download the SSH key')
- public_key = forms.CharField(
- label=_('ssh public key'), max_length=5000, required=False,
- widget=forms.Textarea(attrs={'placeholder': _('ssh-rsa AAAA...')}),
- help_text=_('Paste your id_rsa.pub here.')
- )
-
- def __init__(self, *args, **kwargs):
- if 'instance' in kwargs:
- self.instance = kwargs.pop('instance')
- else:
- self.instance = None
- super().__init__(*args, **kwargs)
-
- def clean_public_key(self):
- public_key = self.cleaned_data['public_key']
- if self.instance.public_key and public_key == self.instance.public_key:
- msg = _('Public key should not be the same as your old one.')
- raise forms.ValidationError(msg)
-
- if public_key and not validate_ssh_public_key(public_key):
- raise forms.ValidationError(_('Not a valid ssh public key'))
- return public_key
-
- def save(self):
- public_key = self.cleaned_data['public_key']
- if public_key:
- self.instance.public_key = public_key
- self.instance.save()
- return self.instance
-
-
-UserPublicKeyForm.verbose_name = _("Public key")
-
-
class UserBulkUpdateForm(OrgModelForm):
users = forms.ModelMultipleChoiceField(
required=True,
@@ -333,40 +188,12 @@ class UserBulkUpdateForm(OrgModelForm):
return users
-class UserGroupForm(OrgModelForm):
- users = forms.ModelMultipleChoiceField(
- queryset=User.objects.none(),
- label=_("User"),
- widget=forms.SelectMultiple(
- attrs={
- 'class': 'users-select2',
- 'data-placeholder': _('Select users')
- }
- ),
- required=False,
+class UserCheckPasswordForm(forms.Form):
+ password = forms.CharField(
+ label=_('Password'), widget=forms.PasswordInput,
+ max_length=128, strip=False
)
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self.set_fields_queryset()
- def set_fields_queryset(self):
- users_field = self.fields.get('users')
- if self.instance:
- users_field.initial = self.instance.users.all()
- users_field.queryset = self.instance.users.all()
- else:
- users_field.queryset = User.objects.none()
-
- def save(self, commit=True):
- raise Exception("Save by restful api")
-
- class Meta:
- model = UserGroup
- fields = [
- 'name', 'users', 'comment',
- ]
-
-
-class FileForm(forms.Form):
- file = forms.FileField()
+class UserCheckOtpCodeForm(forms.Form):
+ otp_code = forms.CharField(label=_('MFA code'), max_length=6)
diff --git a/apps/users/hands.py b/apps/users/hands.py
index 0792fa099..5e2007c8a 100644
--- a/apps/users/hands.py
+++ b/apps/users/hands.py
@@ -11,7 +11,6 @@
"""
# from terminal.models import Terminal
-# from audits.tasks import write_login_log_async
# from users.models import User
# from perms.models import AssetPermission
# from perms.utils import get_user_granted_assets, get_user_granted_asset_groups
diff --git a/apps/users/models/group.py b/apps/users/models/group.py
index e211759bc..0ae636f76 100644
--- a/apps/users/models/group.py
+++ b/apps/users/models/group.py
@@ -4,6 +4,7 @@ import uuid
from django.db import models, IntegrityError
from django.utils.translation import ugettext_lazy as _
+from common.utils import lazyproperty
from orgs.mixins.models import OrgModelMixin
__all__ = ['UserGroup']
@@ -20,6 +21,10 @@ class UserGroup(OrgModelMixin):
def __str__(self):
return self.name
+ @lazyproperty
+ def users_amount(self):
+ return self.users.count()
+
class Meta:
ordering = ['name']
unique_together = [('org_id', 'name'),]
diff --git a/apps/users/models/user.py b/apps/users/models/user.py
index ba4668f7f..a062f6c99 100644
--- a/apps/users/models/user.py
+++ b/apps/users/models/user.py
@@ -17,14 +17,13 @@ from django.utils import timezone
from django.shortcuts import reverse
from orgs.utils import current_org
-from common.utils import get_signer, date_expired_default, get_logger, lazyproperty
+from common.utils import signer, date_expired_default, get_logger, lazyproperty
from common import fields
+from ..signals import post_user_change_password
__all__ = ['User']
-signer = get_signer()
-
logger = get_logger(__file__)
@@ -43,14 +42,10 @@ class AuthMixin:
self.set_password(password_raw_)
def set_password(self, raw_password):
- self._set_password = True
if self.can_update_password():
self.date_password_last_updated = timezone.now()
+ post_user_change_password.send(self.__class__, user=self)
super().set_password(raw_password)
- else:
- error = _("User auth from {}, go there change password").format(
- self.source)
- raise PermissionError(error)
def can_update_password(self):
return self.is_local
@@ -196,22 +191,22 @@ class RoleMixin:
def is_app(self):
return self.role == 'App'
- @property
+ @lazyproperty
def user_orgs(self):
from orgs.models import Organization
return Organization.get_user_user_orgs(self)
- @property
+ @lazyproperty
def admin_orgs(self):
from orgs.models import Organization
return Organization.get_user_admin_orgs(self)
- @property
+ @lazyproperty
def audit_orgs(self):
from orgs.models import Organization
return Organization.get_user_audit_orgs(self)
- @property
+ @lazyproperty
def admin_or_audit_orgs(self):
from orgs.models import Organization
return Organization.get_user_admin_or_audit_orgs(self)
@@ -223,26 +218,26 @@ class RoleMixin:
else:
return False
- @property
+ @lazyproperty
def is_org_auditor(self):
if self.is_super_auditor or self.related_audit_orgs.exists():
return True
else:
return False
- @property
+ @lazyproperty
def can_admin_current_org(self):
return current_org.can_admin_by(self)
- @property
+ @lazyproperty
def can_audit_current_org(self):
return current_org.can_audit_by(self)
- @property
+ @lazyproperty
def can_user_current_org(self):
return current_org.can_user_by(self)
- @property
+ @lazyproperty
def can_admin_or_audit_current_org(self):
return self.can_admin_current_org or self.can_audit_current_org
@@ -267,6 +262,16 @@ class RoleMixin:
access_key = app.create_access_key()
return app, access_key
+ def remove(self):
+ if not current_org.is_real():
+ return
+ if self.can_user_current_org:
+ current_org.users.remove(self)
+ if self.can_admin_current_org:
+ current_org.admins.remove(self)
+ if self.can_audit_current_org:
+ current_org.auditors.remove(self)
+
class TokenMixin:
CACHE_KEY_USER_RESET_PASSWORD_PREFIX = "_KEY_USER_RESET_PASSWORD_{}"
@@ -384,7 +389,7 @@ class MFAMixin:
@staticmethod
def mfa_is_otp():
- if settings.CONFIG.OTP_IN_RADIUS:
+ if settings.OTP_IN_RADIUS:
return False
return True
@@ -401,7 +406,7 @@ class MFAMixin:
return check_otp_code(self.otp_secret_key, code)
def check_mfa(self, code):
- if settings.CONFIG.OTP_IN_RADIUS:
+ if settings.OTP_IN_RADIUS:
return self.check_radius(code)
else:
return self.check_otp(code)
@@ -540,6 +545,9 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
return True
return False
+ def set_avatar(self, f):
+ self.avatar.save(self.username, f)
+
def avatar_url(self):
admin_default = settings.STATIC_URL + "img/avatar/admin.png"
user_default = settings.STATIC_URL + "img/avatar/user.png"
diff --git a/apps/users/serializers/group.py b/apps/users/serializers/group.py
index c817de289..67b21668c 100644
--- a/apps/users/serializers/group.py
+++ b/apps/users/serializers/group.py
@@ -4,29 +4,29 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
-from common.fields import StringManyToManyField
from common.serializers import AdaptedBulkListSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
+from django.db.models import Count
from ..models import User, UserGroup
from .. import utils
__all__ = [
- 'UserGroupSerializer', 'UserGroupListSerializer',
- 'UserGroupUpdateMemberSerializer',
+ 'UserGroupSerializer',
]
class UserGroupSerializer(BulkOrgResourceModelSerializer):
users = serializers.PrimaryKeyRelatedField(
- required=False, many=True, queryset=User.objects, label=_('User')
+ required=False, many=True, queryset=User.objects, label=_('User'),
+ write_only=True
)
class Meta:
model = UserGroup
list_serializer_class = AdaptedBulkListSerializer
fields = [
- 'id', 'name', 'users', 'comment', 'date_created',
- 'created_by',
+ 'id', 'name', 'users', 'users_amount', 'comment',
+ 'date_created', 'created_by',
]
extra_kwargs = {
'created_by': {'label': _('Created by'), 'read_only': True}
@@ -47,23 +47,8 @@ class UserGroupSerializer(BulkOrgResourceModelSerializer):
raise serializers.ValidationError(msg)
return users
-
-class UserGroupListSerializer(UserGroupSerializer):
- users = StringManyToManyField(many=True, read_only=True)
-
-
-class UserGroupUpdateMemberSerializer(serializers.ModelSerializer):
- users = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects)
-
- class Meta:
- model = UserGroup
- fields = ['id', 'users']
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.set_fields_queryset()
-
- def set_fields_queryset(self):
- users_field = self.fields['users']
- users_field.child_relation.queryset = utils.get_current_org_members()
-
+ @classmethod
+ def setup_eager_loading(cls, queryset):
+ """ Perform necessary eager loading of data. """
+ queryset = queryset.annotate(users_amount=Count('users'))
+ return queryset
diff --git a/apps/users/serializers/realtion.py b/apps/users/serializers/realtion.py
index 15cc9e43c..b768c57fc 100644
--- a/apps/users/serializers/realtion.py
+++ b/apps/users/serializers/realtion.py
@@ -8,9 +8,11 @@ __all__ = ['UserUserGroupRelationSerializer']
class UserUserGroupRelationSerializer(serializers.ModelSerializer):
- user_name = serializers.CharField(read_only=True)
- usergroup_name = serializers.CharField(read_only=True)
+ user_display = serializers.CharField(read_only=True)
+ usergroup_display = serializers.CharField(read_only=True)
class Meta:
model = User.groups.through
- fields = ['id', 'user', 'user_name', 'usergroup', 'usergroup_name']
+ fields = [
+ 'id', 'user', 'user_display', 'usergroup', 'usergroup_display'
+ ]
diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py
index 256dc073d..2363c39ed 100644
--- a/apps/users/serializers/user.py
+++ b/apps/users/serializers/user.py
@@ -7,11 +7,11 @@ from common.utils import validate_ssh_public_key
from common.mixins import BulkSerializerMixin
from common.serializers import AdaptedBulkListSerializer
from common.permissions import CanUpdateDeleteUser
-from ..models import User, UserGroup
+from ..models import User
__all__ = [
- 'UserSerializer', 'UserPKUpdateSerializer', 'UserUpdateGroupSerializer',
+ 'UserSerializer', 'UserPKUpdateSerializer',
'ChangeUserPasswordSerializer', 'ResetOTPSerializer',
'UserProfileSerializer', 'UserDisplaySerializer',
]
@@ -123,16 +123,6 @@ class UserPKUpdateSerializer(serializers.ModelSerializer):
return value
-class UserUpdateGroupSerializer(serializers.ModelSerializer):
- groups = serializers.PrimaryKeyRelatedField(
- many=True, queryset=UserGroup.objects
- )
-
- class Meta:
- model = User
- fields = ['id', 'groups']
-
-
class ChangeUserPasswordSerializer(serializers.ModelSerializer):
class Meta:
diff --git a/apps/users/signals.py b/apps/users/signals.py
index b9084b7dc..37969f839 100644
--- a/apps/users/signals.py
+++ b/apps/users/signals.py
@@ -2,3 +2,4 @@ from django.dispatch import Signal
post_user_create = Signal(providing_args=('user',))
+post_user_change_password = Signal(providing_args=('user',))
diff --git a/apps/users/signals_handler.py b/apps/users/signals_handler.py
index a33d9eec9..902dfa4d7 100644
--- a/apps/users/signals_handler.py
+++ b/apps/users/signals_handler.py
@@ -2,7 +2,7 @@
#
from django.dispatch import receiver
-from django.db.models.signals import post_save, m2m_changed
+from django.db.models.signals import m2m_changed
from common.utils import get_logger
from .signals import post_user_create
diff --git a/apps/users/templates/users/_base_otp.html b/apps/users/templates/users/_base_otp.html
index ac511efb0..89ac8d656 100644
--- a/apps/users/templates/users/_base_otp.html
+++ b/apps/users/templates/users/_base_otp.html
@@ -1,60 +1,20 @@
+{% extends '_without_nav_base.html' %}
{% load static %}
{% load i18n %}
-
-
-
-
- {{ JMS_TITLE }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% block small_title %}
- {% endblock %}
-
-
-
-
{% trans 'Security token validation' %} {% trans 'Account' %} {{ user.username }} {% trans 'Follow these steps to complete the binding operation' %}
-
- {% block content %}
+{% block body %}
+
+
+
+ {% block small_title %}
{% endblock %}
-
-
-
-
-
-
-
-
-
+
+
+
+
{% trans 'Security token validation' %} {% trans 'Account' %} {{ user.username }} {% trans 'Follow these steps to complete the binding operation' %}
+
+ {% block content %}
+ {% endblock %}
+
+
+{% endblock %}
diff --git a/apps/users/templates/users/_base_user_detail.html b/apps/users/templates/users/_base_user_detail.html
new file mode 100644
index 000000000..936037ab8
--- /dev/null
+++ b/apps/users/templates/users/_base_user_detail.html
@@ -0,0 +1,26 @@
+{% extends 'base.html' %}
+{% load static %}
+{% load i18n %}
+
+{% block content %}
+
+
+
+
+
+
+ {% include 'users/_user_detail_nav_header.html' %}
+
+ {% block content_nav_delete_update %}
+ {% endblock %}
+
+
+
+ {% block content_table %}
+ {% endblock %}
+
+
+
+
+
+{% endblock %}
diff --git a/apps/users/templates/users/_user_detail_nav_header.html b/apps/users/templates/users/_user_detail_nav_header.html
new file mode 100644
index 000000000..28079e149
--- /dev/null
+++ b/apps/users/templates/users/_user_detail_nav_header.html
@@ -0,0 +1,97 @@
+{% load static %}
+{% load i18n %}
+
+
+
+
+ {% trans 'User detail' %}
+
+
+
+ {% trans "User permissions" %}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/users/templates/users/_user_groups_import_modal.html b/apps/users/templates/users/_user_groups_import_modal.html
deleted file mode 100644
index 63d057215..000000000
--- a/apps/users/templates/users/_user_groups_import_modal.html
+++ /dev/null
@@ -1,6 +0,0 @@
-{% extends '_import_modal.html' %}
-{% load i18n %}
-
-{% block modal_title%}{% trans "Import user groups" %}{% endblock %}
-
-{% block import_modal_download_template_url %}{% url "api-users:user-group-list" %}{% endblock %}
\ No newline at end of file
diff --git a/apps/users/templates/users/_user_groups_update_modal.html b/apps/users/templates/users/_user_groups_update_modal.html
deleted file mode 100644
index a07c0f82c..000000000
--- a/apps/users/templates/users/_user_groups_update_modal.html
+++ /dev/null
@@ -1,4 +0,0 @@
-{% extends '_update_modal.html' %}
-{% load i18n %}
-
-{% block modal_title%}{% trans "Update user group" %}{% endblock %}
\ No newline at end of file
diff --git a/apps/users/templates/users/_user_import_modal.html b/apps/users/templates/users/_user_import_modal.html
deleted file mode 100644
index e53d67fa7..000000000
--- a/apps/users/templates/users/_user_import_modal.html
+++ /dev/null
@@ -1,6 +0,0 @@
-{% extends '_import_modal.html' %}
-{% load i18n %}
-
-{% block modal_title%}{% trans "Import users" %}{% endblock %}
-
-{% block import_modal_download_template_url %}{% url "api-users:user-list" %}{% endblock %}
diff --git a/apps/users/templates/users/_user_update_modal.html b/apps/users/templates/users/_user_update_modal.html
deleted file mode 100644
index 9dfe60c96..000000000
--- a/apps/users/templates/users/_user_update_modal.html
+++ /dev/null
@@ -1,4 +0,0 @@
-{% extends '_update_modal.html' %}
-{% load i18n %}
-
-{% block modal_title%}{% trans "Update user" %}{% endblock %}
\ No newline at end of file
diff --git a/apps/users/templates/users/first_login.html b/apps/users/templates/users/first_login.html
index 1038fb8c8..fb8af6257 100644
--- a/apps/users/templates/users/first_login.html
+++ b/apps/users/templates/users/first_login.html
@@ -13,7 +13,7 @@
{% block content %}
-
+
{% trans 'First Login' %}
@@ -55,7 +55,7 @@
-
+
{% csrf_token %}
{{ wizard.management_form }}
{#{% if wizard.form.forms %}#}
@@ -88,7 +88,7 @@
{% endif %}
-
+
diff --git a/apps/users/templates/users/first_login_done.html b/apps/users/templates/users/first_login_done.html
index 5639d2c5d..ae43d032b 100644
--- a/apps/users/templates/users/first_login_done.html
+++ b/apps/users/templates/users/first_login_done.html
@@ -13,7 +13,7 @@
{% block content %}
-
+
{% trans 'First Login' %}
diff --git a/apps/users/templates/users/forgot_password.html b/apps/users/templates/users/forgot_password.html
index 3f3c7fabb..d48cf0277 100644
--- a/apps/users/templates/users/forgot_password.html
+++ b/apps/users/templates/users/forgot_password.html
@@ -1,60 +1,34 @@
+{% extends '_base_only_content.html' %}
{% load static %}
{% load i18n %}
-
-
+{% load bootstrap3 %}
+{% block custom_head_css_js %}
+
+{% endblock %}
+{% block html_title %}{% trans 'Forgot password' %}{% endblock %}
+{% block title %} {% trans 'Forgot password' %}?{% endblock %}
-
-
-
-
-
{% trans 'Forgot password' %}
+{% block content %}
+ {% if errors %}
+
{{ errors }}
+ {% endif %}
+
+ {% trans 'Input your email, that will send a mail to your' %}
+
- {% include '_head_css_js.html' %}
-
-
-
-
-
-
-
-
-
-
-
-
-
{% trans 'Forgot password' %} ?
-
- {% if errors %}
-
{{ errors }}
- {% endif %}
-
- {% trans 'Input your email, that will send a mail to your' %}
-
-
-
-
-
- {% csrf_token %}
-
-
-
-
- {% trans 'Submit' %}
-
-
-
-
-
-
-
-
-
-
- {% include '_copyright.html' %}
-
+
+
+
+ {% csrf_token %}
+ {% bootstrap_field form.email layout="horizontal" %}
+ {% bootstrap_field form.captcha layout="horizontal" %}
+ {% trans 'Submit' %}
+
+{% endblock %}
-
-
-
diff --git a/apps/users/templates/users/reset_password.html b/apps/users/templates/users/reset_password.html
index 6817f0a75..ecad676bf 100644
--- a/apps/users/templates/users/reset_password.html
+++ b/apps/users/templates/users/reset_password.html
@@ -1,136 +1,80 @@
+{% extends '_base_only_content.html' %}
{% load static %}
{% load i18n %}
-
-
+{% load bootstrap3 %}
+{% block html_title %}{% trans 'Reset password' %}{% endblock %}
+{% block title %}{% trans 'Reset password' %}{% endblock %}
-
-
-
-
{{ JMS_TITLE }}
-
- {% include '_head_css_js.html' %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{% trans 'Welcome to the Jumpserver open source fortress' %}
-
-
- {% trans 'Jumpserver is an open source desktop system developed using Python and Django that helps Internet businesses with efficient users, assets, permissions, and audit management' %}
-
-
-
- {% trans 'We are from all over the world, we have great admiration and worship for the spirit of open source, we have endless pursuit for perfection, neatness and elegance' %}
-
-
-
- {% trans 'We focus on automatic operation and maintenance, and strive to build an easy-to-use, stable, safe and automatic board hopping machine, which is our unremitting pursuit and power' %}
-
-
-
- {% trans 'Always young, always with tears in my eyes. Stay foolish Stay hungry' %}
-
-
-
-
-
-
{% trans 'Reset password' %}
-
- {% csrf_token %}
- {% if errors %}
- {{ errors }}
- {% endif %}
-
-
-
-
- {% trans "Setting" %}
-
-
- Forgot password?
-
-
-
-
-
-
-
+{% block content %}
+
+ {% csrf_token %}
+ {% if errors %}
+ {{ errors }}
+ {% endif %}
+ {% if not token_invalid %}
+
-
-
-
- {% include '_copyright.html' %}
-
-
-
+
{% trans "Setting" %}
+ {% endif %}
+
+{% endblock %}
-
+{% block custom_foot_js %}
+
+
+{% endblock %}
diff --git a/apps/users/templates/users/user_asset_permission.html b/apps/users/templates/users/user_asset_permission.html
new file mode 100644
index 000000000..21ae72722
--- /dev/null
+++ b/apps/users/templates/users/user_asset_permission.html
@@ -0,0 +1,201 @@
+{% extends 'users/_base_user_detail.html' %}
+{% load static %}
+{% load i18n %}
+
+{% block custom_head_css_js %}
+
+
+{% endblock %}
+
+
+{% block content_table %}
+
+
+
+
+
+
+
+
+ {% trans 'Name' %}
+ {% trans 'User' %}
+ {% trans 'User group' %}
+ {% trans 'Asset' %}
+ {% trans 'Node' %}
+ {% trans 'System user' %}
+ {% trans 'Validity' %}
+ {% trans 'Action' %}
+
+
+
+
+
+
+
+
+
+{% include '_filter_dropdown.html' %}
+
+{% endblock %}
+
+{% block custom_foot_js %}
+
+{% endblock %}
diff --git a/apps/users/templates/users/user_database_app_permission.html b/apps/users/templates/users/user_database_app_permission.html
new file mode 100644
index 000000000..73b3a7972
--- /dev/null
+++ b/apps/users/templates/users/user_database_app_permission.html
@@ -0,0 +1,168 @@
+{% extends 'users/_base_user_detail.html' %}
+{% load static %}
+{% load i18n %}
+
+{% block custom_head_css_js %}
+
+
+{% endblock %}
+
+{% block content_table %}
+
+
+
+
+
+
+
+
+ {% trans 'Name' %}
+ {% trans 'User' %}
+ {% trans 'User group' %}
+ {% trans 'DatabaseApp' %}
+ {% trans 'System user' %}
+ {% trans 'Validity' %}
+ {% trans 'Action' %}
+
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block custom_foot_js %}
+
+{% endblock %}
diff --git a/apps/users/templates/users/user_detail.html b/apps/users/templates/users/user_detail.html
index 2a6ed62b1..7bac7a454 100644
--- a/apps/users/templates/users/user_detail.html
+++ b/apps/users/templates/users/user_detail.html
@@ -1,345 +1,301 @@
-{% extends 'base.html' %}
+{% extends 'users/_base_user_detail.html' %}
{% load static %}
{% load i18n %}
{% block custom_head_css_js %}
-
-
{% endblock %}
-{% block content %}
-
-
-
-
-
-
-
-
-
-
{{ user_object.name }}
-
-
-
-
-
-
-
-
-
-
-
- {% trans 'Name' %}:
- {{ user_object.name }}
-
-
- {% trans 'Username' %}:
- {{ user_object.username }}
-
-
- {% trans 'Email' %}:
- {{ user_object.email }}
-
- {% if user.phone %}
-
- {% trans 'Phone' %}:
- {{ user_object.phone }}
-
- {% endif %}
- {% if user_object.wechat %}
-
- {% trans 'Wechat' %}:
- {{ user_object.wechat }}
-
- {% endif %}
-
- {% trans 'Role' %}:
- {{ user_object.role_display }}
-
-
- {% trans 'MFA certification' %}:
-
- {% if user_object.mfa_force_enabled %}
- {% trans 'Force enabled' %}
- {% elif user_object.mfa_enabled%}
- {% trans 'Enabled' %}
- {% else %}
- {% trans 'Disabled' %}
- {% endif %}
-
-
-
- {% trans 'Source' %}:
- {{ user_object.get_source_display }}
-
-
- {% trans 'Date expired' %}:
- {{ user_object.date_expired|date:"Y-m-j H:i:s" }}
-
-
- {% trans 'Created by' %}:
- {{ user_object.created_by }}
-
-
- {% trans 'Date joined' %}:
- {{ user_object.date_joined|date:"Y-m-j H:i:s" }}
-
-
- {% trans 'Last login' %}:
- {{ user_object.last_login|date:"Y-m-j H:i:s" }}
-
- {% if user_object.can_update_password %}
-
- {% trans 'Last password updated' %}:
- {{ user_object.date_password_last_updated|date:"Y-m-j H:i:s" }}
-
- {% endif %}
-
- {% trans 'Comment' %}:
- {{ user_object.comment }}
-
-
-
-
-
-
-
-
-
- {% trans 'Quick modify' %}
-
-
-
-
-
- {% trans 'Active' %}:
-
-
-
-
-
- {% trans 'Force enabled MFA' %}:
-
-
-
-
-
-
-
- {% trans 'Reset MFA' %}:
-
-
- {% trans 'Reset' %}
-
-
-
- {% if user_object.can_update_password %}
-
- {% trans 'Send reset password mail' %}:
-
-
- {% trans 'Send' %}
-
-
-
- {% endif %}
- {% if user_object.can_update_ssh_key %}
-
- {% trans 'Send reset ssh key mail' %}:
-
-
- {% trans 'Send' %}
-
-
-
- {% endif %}
-
- {% trans 'Unblock user' %}
-
-
- {% trans 'Unblock' %}
-
-
-
-
-
-
-
- {% if request.user.can_admin_current_org %}
+{% block content_nav_delete_update %}
+
+ {% trans 'Update' %}
+
+
+
+ {% trans 'Delete' %}
+
+
+{% endblock %}
- {% if user_object.can_user_current_org or user_object.can_admin_current_org %}
-
-
- {% trans 'User group' %}
-
-
-
-
-
-
-
-
- {% for group in groups %}
- {{ group.name }}
- {% endfor %}
-
-
-
-
-
- {% trans 'Join' %}
-
-
-
-
- {% for group in user_object.groups.all %}
-
-
- {{ group.name }}
-
-
-
-
-
- {% endfor %}
-
-
-
-
- {% endif %}
-
- {% if LICENSE_VALID and LOGIN_CONFIRM_ENABLE %}
-
-
- {% trans 'Login confirm' %}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% trans 'Confirm' %}
-
-
-
- {% if user_object.get_login_confirm_setting %}
- {% for u in user_object.login_confirm_setting.reviewers.all %}
-
-
- {{ u }}
-
-
-
-
-
- {% endfor %}
- {% endif %}
-
-
-
-
- {% endif %}
-
- {% endif %}
-
-
+{% block content_table %}
+
+
+
+
+
+
+
+
+
+
+
+
+ {% trans 'Name' %}:
+ {{ object.name }}
+
+
+ {% trans 'Username' %}:
+ {{ object.username }}
+
+
+ {% trans 'Email' %}:
+ {{ object.email }}
+
+ {% if user.phone %}
+
+ {% trans 'Phone' %}:
+ {{ object.phone }}
+
+ {% endif %}
+ {% if object.wechat %}
+
+ {% trans 'Wechat' %}:
+ {{ object.wechat }}
+
+ {% endif %}
+
+ {% trans 'Role' %}:
+ {{ object.role_display }}
+
+
+ {% trans 'MFA certification' %}:
+
+ {% if object.mfa_force_enabled %}
+ {% trans 'Force enabled' %}
+ {% elif object.mfa_enabled%}
+ {% trans 'Enabled' %}
+ {% else %}
+ {% trans 'Disabled' %}
+ {% endif %}
+
+
+
+ {% trans 'Source' %}:
+ {{ object.get_source_display }}
+
+
+ {% trans 'Date expired' %}:
+ {{ object.date_expired|date:"Y-m-j H:i:s" }}
+
+
+ {% trans 'Created by' %}:
+ {{ object.created_by }}
+
+
+ {% trans 'Date joined' %}:
+ {{ object.date_joined|date:"Y-m-j H:i:s" }}
+
+
+ {% trans 'Last login' %}:
+ {{ object.last_login|date:"Y-m-j H:i:s" }}
+
+ {% if object.can_update_password %}
+
+ {% trans 'Last password updated' %}:
+ {{ object.date_password_last_updated|date:"Y-m-j H:i:s" }}
+
+ {% endif %}
+
+ {% trans 'Comment' %}:
+ {{ object.comment }}
+
+
+
+
- {% include 'users/_user_update_pk_modal.html' %}
+
+
+
+ {% trans 'Quick modify' %}
+
+
+
+
+
+ {% trans 'Active' %}:
+
+
+
+
+
+
+
+ {% trans 'Force enabled MFA' %}:
+
+
+
+
+
+
+
+ {% trans 'Reset MFA' %}:
+
+
+ {% trans 'Reset' %}
+
+
+
+ {% if object.can_update_password %}
+
+ {% trans 'Send reset password mail' %}:
+
+
+ {% trans 'Send' %}
+
+
+
+ {% endif %}
+ {% if object.can_update_ssh_key %}
+
+ {% trans 'Send reset ssh key mail' %}:
+
+
+ {% trans 'Send' %}
+
+
+
+ {% endif %}
+
+ {% trans 'Unblock user' %}
+
+
+ {% trans 'Unblock' %}
+
+
+
+
+
+
+
+ {% if request.user.can_admin_current_org %}
+
+ {% if object.can_user_current_org or object.can_admin_current_org %}
+
+
+ {% trans 'User group' %}
+
+
+
+
+
+
+
+
+ {% for group in groups %}
+ {{ group.name }}
+ {% endfor %}
+
+
+
+
+
+ {% trans 'Join' %}
+
+
+
+
+ {% for group in object.groups.all %}
+
+
+ {{ group.name }}
+
+
+
+
+
+ {% endfor %}
+
+
+
+
+ {% endif %}
+
+ {% if LICENSE_VALID and LOGIN_CONFIRM_ENABLE %}
+
+
+ {% trans 'Login confirm' %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% trans 'Confirm' %}
+
+
+
+ {% if object.get_login_confirm_setting %}
+ {% for u in object.login_confirm_setting.reviewers.all %}
+
+
+ {{ u }}
+
+
+
+
+
+ {% endfor %}
+ {% endif %}
+
+
+
+
+ {% endif %}
+
+ {% endif %}
+
+
+{% include 'users/_user_update_pk_modal.html' %}
+
{% endblock %}
+
{% block custom_foot_js %}
{% endblock %}
-{% block content %}
-
-
-
-
-
-
- {% include 'users/_granted_assets.html' %}
-
-
-
-
-
+
+{% block content_table %}
+{% include 'users/_granted_assets.html' %}
{% endblock %}
+
{% block custom_foot_js %}
+{% endblock %}
+
+{% block content_table %}
+
+{% endblock %}
+
+{% block custom_foot_js %}
+
+{% endblock %}
diff --git a/apps/users/templates/users/user_granted_remote_app.html b/apps/users/templates/users/user_granted_remote_app.html
new file mode 100644
index 000000000..19b8ab22c
--- /dev/null
+++ b/apps/users/templates/users/user_granted_remote_app.html
@@ -0,0 +1,93 @@
+{% extends 'users/_base_user_detail.html' %}
+{% load i18n static %}
+
+{% block custom_head_css_js %}
+
+{% endblock %}
+
+{% block content_table %}
+
+{% endblock %}
+
+{% block custom_foot_js %}
+
+{% endblock %}
diff --git a/apps/users/templates/users/user_group_create_update.html b/apps/users/templates/users/user_group_create_update.html
index 25f027ad5..09030eca8 100644
--- a/apps/users/templates/users/user_group_create_update.html
+++ b/apps/users/templates/users/user_group_create_update.html
@@ -2,10 +2,6 @@
{% load static %}
{% load i18n %}
{% load bootstrap3 %}
-{% block custom_head_css_js %}
-
-
-{% endblock %}
{% block content %}
diff --git a/apps/users/templates/users/user_group_detail.html b/apps/users/templates/users/user_group_detail.html
index 8f5cccf29..cd65bb746 100644
--- a/apps/users/templates/users/user_group_detail.html
+++ b/apps/users/templates/users/user_group_detail.html
@@ -3,13 +3,8 @@
{% load i18n %}
{% block custom_head_css_js %}
-
-
-
-
-
{% endblock %}
{% block content %}
@@ -96,9 +91,9 @@
{% for user in user_group.users.all %}
- {{ user.name }}
+ {{ user.name }}
-
+
{% endfor %}
@@ -115,73 +110,51 @@
{% endblock %}
{% block custom_foot_js %}
{% endblock %}
diff --git a/apps/users/templates/users/user_group_list.html b/apps/users/templates/users/user_group_list.html
index 3eb70e319..5e354d3db 100644
--- a/apps/users/templates/users/user_group_list.html
+++ b/apps/users/templates/users/user_group_list.html
@@ -1,28 +1,7 @@
{% extends '_base_list.html' %}
{% load i18n static %}
{% block table_search %}
-
+ {% include '_csv_import_export.html' %}
{% endblock %}
{% block table_container %}
@@ -39,14 +18,13 @@
-{% include "users/_user_groups_import_modal.html" %}
-{% include "users/_user_groups_update_modal.html" %}
{% endblock %}
{% block content_bottom_left %}{% endblock %}
{% block custom_foot_js %}
{% endblock %}
diff --git a/apps/users/templates/users/user_list.html b/apps/users/templates/users/user_list.html
index 265d4a4db..63dd981be 100644
--- a/apps/users/templates/users/user_list.html
+++ b/apps/users/templates/users/user_list.html
@@ -1,28 +1,7 @@
{% extends '_base_list.html' %}
{% load i18n static %}
{% block table_search %}
-
+ {% include '_csv_import_export.html' %}
{% endblock %}
{% block table_container %}
@@ -47,7 +26,11 @@
- {% trans 'Delete selected' %}
+ {% if CURRENT_ORG.is_default %}
+ {% trans 'Delete selected' %}
+ {% else %}
+ {% trans 'Remove selected' %}
+ {% endif %}
{% trans 'Update selected' %}
{% trans 'Deactive selected' %}
{% trans 'Active selected' %}
@@ -59,14 +42,12 @@
-{% include "users/_user_import_modal.html" %}
-{% include "users/_user_update_modal.html" %}
{% endblock %}
{% block content_bottom_left %}{% endblock %}
{% block custom_foot_js %}
+{% endblock %}
+
+{% block content_table %}
+
+
+
+
+
+
+
+
+ {% trans 'Name' %}
+ {% trans 'User' %}
+ {% trans 'User group' %}
+ {% trans 'RemoteApp' %}
+ {% trans 'System user' %}
+ {% trans 'Validity' %}
+ {% trans 'Action' %}
+
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block custom_foot_js %}
+
+{% endblock %}
diff --git a/apps/users/urls/api_urls.py b/apps/users/urls/api_urls.py
index f0fe3db6f..b3a75f9ba 100644
--- a/apps/users/urls/api_urls.py
+++ b/apps/users/urls/api_urls.py
@@ -14,7 +14,7 @@ app_name = 'users'
router = BulkRouter()
router.register(r'users', api.UserViewSet, 'user')
router.register(r'groups', api.UserGroupViewSet, 'user-group')
-router.register(r'users-groups-relations', api.UserUserGroupRelationViewSet, 'user-group-relation')
+router.register(r'users-groups-relations', api.UserUserGroupRelationViewSet, 'users-groups-relation')
urlpatterns = [
@@ -28,8 +28,6 @@ urlpatterns = [
path('users//pubkey/reset/', api.UserResetPKApi.as_view(), name='user-public-key-reset'),
path('users//pubkey/update/', api.UserUpdatePKApi.as_view(), name='user-public-key-update'),
path('users//unblock/', api.UserUnblockPKApi.as_view(), name='user-unblock'),
- path('users//groups/', api.UserUpdateGroupApi.as_view(), name='user-update-group'),
- path('groups//users/', api.UserGroupUpdateUserApi.as_view(), name='user-group-update-user'),
]
urlpatterns += router.urls
diff --git a/apps/users/urls/views_urls.py b/apps/users/urls/views_urls.py
index dbed09888..7773ca7d6 100644
--- a/apps/users/urls/views_urls.py
+++ b/apps/users/urls/views_urls.py
@@ -20,10 +20,10 @@ urlpatterns = [
path('profile/password/update/', views.UserPasswordUpdateView.as_view(), name='user-password-update'),
path('profile/pubkey/update/', views.UserPublicKeyUpdateView.as_view(), name='user-pubkey-update'),
path('profile/pubkey/generate/', views.UserPublicKeyGenerateView.as_view(), name='user-pubkey-generate'),
- path('profile/otp/enable/authentication/', views.UserOtpEnableAuthenticationView.as_view(), name='user-otp-enable-authentication'),
+ path('profile/otp/enable/authentication/', views.UserCheckPasswordView.as_view(), name='user-otp-enable-authentication'),
path('profile/otp/enable/install-app/', views.UserOtpEnableInstallAppView.as_view(), name='user-otp-enable-install-app'),
path('profile/otp/enable/bind/', views.UserOtpEnableBindView.as_view(), name='user-otp-enable-bind'),
- path('profile/otp/disable/authentication/', views.UserOtpDisableAuthenticationView.as_view(), name='user-otp-disable-authentication'),
+ path('profile/otp/disable/authentication/', views.UserDisableMFAView.as_view(), name='user-otp-disable-authentication'),
path('profile/otp/update/', views.UserOtpUpdateView.as_view(), name='user-otp-update'),
path('profile/otp/settings-success/', views.UserOtpSettingsSuccessView.as_view(), name='user-otp-settings-success'),
@@ -35,6 +35,11 @@ urlpatterns = [
path('user/update/', views.UserBulkUpdateView.as_view(), name='user-bulk-update'),
path('user//', views.UserDetailView.as_view(), name='user-detail'),
path('user//assets/', views.UserGrantedAssetView.as_view(), name='user-granted-asset'),
+ path('user//asset-permissions/', views.UserAssetPermissionListView.as_view(), name='user-asset-permission'),
+ path('user//remote-apps/', views.UserGrantedRemoteAppView.as_view(), name='user-granted-remote-app'),
+ path('user//remote-app-permissions/', views.UserRemoteAppPermissionListView.as_view(), name='user-remote-app-permission'),
+ path('user//database-apps/', views.UserGrantedDatabasesAppView.as_view(), name='user-granted-database-app'),
+ path('user//database-app-permissions/', views.UserDatabaseAppPermissionListView.as_view(), name='user-database-app-permission'),
path('user//login-history/', views.UserDetailView.as_view(), name='user-login-history'),
# User group view
diff --git a/apps/users/utils.py b/apps/users/utils.py
index a955350a9..5acb4df9a 100644
--- a/apps/users/utils.py
+++ b/apps/users/utils.py
@@ -215,6 +215,12 @@ def set_tmp_user_to_cache(request, user, ttl=3600):
cache.set(request.session.session_key+'user', user, ttl)
+def delete_tmp_user_for_cache(request):
+ if not request.session.session_key:
+ return None
+ cache.delete(request.session.session_key+'user')
+
+
def redirect_user_first_login_or_index(request, redirect_field_name):
if request.user.is_first_login:
return reverse('users:user-first-login')
@@ -329,3 +335,18 @@ def construct_user_email(username, email):
def get_current_org_members(exclude=()):
from orgs.utils import current_org
return current_org.get_org_members(exclude=exclude)
+
+
+def get_source_choices():
+ from .models import User
+ choices_all = dict(User.SOURCE_CHOICES)
+ choices = [
+ (User.SOURCE_LOCAL, choices_all[User.SOURCE_LOCAL]),
+ ]
+ if settings.AUTH_LDAP:
+ choices.append((User.SOURCE_LDAP, choices_all[User.SOURCE_LDAP]))
+ if settings.AUTH_OPENID:
+ choices.append((User.SOURCE_OPENID, choices_all[User.SOURCE_OPENID]))
+ if settings.AUTH_RADIUS:
+ choices.append((User.SOURCE_RADIUS, choices_all[User.SOURCE_RADIUS]))
+ return choices
diff --git a/apps/users/views/__init__.py b/apps/users/views/__init__.py
index b178ac710..17d4f4110 100644
--- a/apps/users/views/__init__.py
+++ b/apps/users/views/__init__.py
@@ -2,4 +2,5 @@
from .login import *
from .user import *
+from .profile import *
from .group import *
diff --git a/apps/users/views/login.py b/apps/users/views/login.py
index 52a721374..c96c73e9d 100644
--- a/apps/users/views/login.py
+++ b/apps/users/views/login.py
@@ -4,13 +4,13 @@ from __future__ import unicode_literals
from django.shortcuts import render
from django.views.generic import RedirectView
from django.core.files.storage import default_storage
-from django.http import HttpResponseRedirect
from django.shortcuts import reverse, redirect
from django.utils.translation import ugettext as _
from django.views.generic.base import TemplateView
from django.conf import settings
from django.urls import reverse_lazy
from formtools.wizard.views import SessionWizardView
+from django.views.generic import FormView
from common.utils import get_object_or_none
from common.permissions import PermissionsMixin, IsValidUser
@@ -33,22 +33,24 @@ class UserLoginView(RedirectView):
query_string = True
-class UserForgotPasswordView(TemplateView):
+class UserForgotPasswordView(FormView):
template_name = 'users/forgot_password.html'
+ form_class = forms.UserForgotPasswordForm
- def post(self, request):
- email = request.POST.get('email')
+ def form_valid(self, form):
+ request = self.request
+ email = form.cleaned_data['email']
user = get_object_or_none(User, email=email)
if not user:
error = _('Email address invalid, please input again')
return self.get(request, errors=error)
elif not user.can_update_password():
- error = _('User auth from {}, go there change password'.format(user.source))
+ error = _('User auth from {}, go there change password'.format(
+ user.source))
return self.get(request, errors=error)
else:
send_reset_password_mail(user)
- return HttpResponseRedirect(
- reverse('users:forgot-password-sendmail-success'))
+ return redirect('users:forgot-password-sendmail-success')
class UserForgotPasswordSendmailSuccessView(TemplateView):
@@ -79,44 +81,47 @@ class UserResetPasswordSuccessView(TemplateView):
return super().get_context_data(**kwargs)
-class UserResetPasswordView(TemplateView):
+class UserResetPasswordView(FormView):
template_name = 'users/reset_password.html'
+ form_class = forms.UserTokenResetPasswordForm
def get(self, request, *args, **kwargs):
- token = request.GET.get('token', '')
+ context = self.get_context_data(**kwargs)
+ errors = kwargs.get('errors')
+ if errors:
+ context['errors'] = errors
+ return self.render_to_response(context)
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ token = self.request.GET.get('token', '')
user = User.validate_reset_password_token(token)
if not user:
- kwargs.update({'errors': _('Token invalid or expired')})
- else:
- check_rules = get_password_check_rules()
- kwargs.update({'password_check_rules': check_rules})
- return super().get(request, *args, **kwargs)
-
- def post(self, request, *args, **kwargs):
- password = request.POST.get('password')
- password_confirm = request.POST.get('password-confirm')
- token = request.GET.get('token')
-
- if password != password_confirm:
- return self.get(request, errors=_('Password not same'))
+ context['errors'] = _('Token invalid or expired')
+ context['token_invalid'] = True
+ check_rules = get_password_check_rules()
+ context['password_check_rules'] = check_rules
+ return context
+ def form_valid(self, form):
+ token = self.request.GET.get('token')
user = User.validate_reset_password_token(token)
if not user:
- return self.get(request, errors=_('Token invalid or expired'))
+ return self.get(self.request, errors=_('Token invalid or expired'))
+
if not user.can_update_password():
- error = _('User auth from {}, go there change password'.format(user.source))
- return self.get(request, errors=error)
+ errors = _('User auth from {}, go there change password'.format(user.source))
+ return self.get(self.request, errors=errors)
+ password = form.cleaned_data['new_password']
is_ok = check_password_rules(password)
if not is_ok:
- return self.get(
- request,
- errors=_('* Your password does not meet the requirements')
- )
+ errors = _('* Your password does not meet the requirements')
+ return self.get(self.request, errors=errors)
user.reset_password(password)
User.expired_reset_password_token(token)
- return HttpResponseRedirect(reverse('users:reset-password-success'))
+ return redirect('users:reset-password-success')
class UserFirstLoginView(PermissionsMixin, SessionWizardView):
@@ -177,5 +182,4 @@ class UserFirstLoginView(PermissionsMixin, SessionWizardView):
choices = [(k, v) for k, v in choices if k in [0, 1]]
form.fields["mfa_level"].choices = choices
form.fields["mfa_level"].initial = self.request.user.mfa_level
-
return form
diff --git a/apps/users/views/profile.py b/apps/users/views/profile.py
new file mode 100644
index 000000000..dc0359fa9
--- /dev/null
+++ b/apps/users/views/profile.py
@@ -0,0 +1,272 @@
+# ~*~ coding: utf-8 ~*~
+
+from __future__ import unicode_literals
+
+
+from django.contrib.auth import authenticate
+from django.core.cache import cache
+from django.conf import settings
+from django.http import HttpResponse
+from django.shortcuts import redirect
+from django.urls import reverse_lazy, reverse
+from django.utils.translation import ugettext as _
+from django.views import View
+from django.views.generic.base import TemplateView
+from django.views.generic.edit import (
+ UpdateView, FormView
+)
+from django.contrib.auth import logout as auth_logout
+
+from common.utils import get_logger, ssh_key_gen
+from common.permissions import (
+ PermissionsMixin, IsValidUser,
+ UserCanUpdatePassword, UserCanUpdateSSHKey,
+)
+from .. import forms
+from ..models import User
+from ..utils import (
+ generate_otp_uri, check_otp_code, get_user_or_tmp_user,
+ delete_tmp_user_for_cache, check_password_rules, get_password_check_rules,
+)
+
+__all__ = [
+ 'UserProfileView',
+ 'UserProfileUpdateView', 'UserPasswordUpdateView',
+ 'UserPublicKeyUpdateView', 'UserPublicKeyGenerateView',
+ 'UserCheckPasswordView', 'UserOtpEnableInstallAppView',
+ 'UserOtpEnableBindView', 'UserOtpSettingsSuccessView',
+ 'UserDisableMFAView', 'UserOtpUpdateView',
+]
+
+logger = get_logger(__name__)
+
+
+class UserProfileView(PermissionsMixin, TemplateView):
+ template_name = 'users/user_profile.html'
+ permission_classes = [IsValidUser]
+
+ def get_context_data(self, **kwargs):
+ mfa_setting = settings.SECURITY_MFA_AUTH
+ context = {
+ 'action': _('Profile'),
+ 'mfa_setting': mfa_setting if mfa_setting is not None else False,
+ }
+ kwargs.update(context)
+ return super().get_context_data(**kwargs)
+
+
+class UserProfileUpdateView(PermissionsMixin, UpdateView):
+ template_name = 'users/user_profile_update.html'
+ model = User
+ permission_classes = [IsValidUser]
+ form_class = forms.UserProfileForm
+ success_url = reverse_lazy('users:user-profile')
+
+ def get_object(self, queryset=None):
+ return self.request.user
+
+ def get_context_data(self, **kwargs):
+ context = {
+ 'app': _('User'),
+ 'action': _('Profile setting'),
+ }
+ kwargs.update(context)
+ return super().get_context_data(**kwargs)
+
+
+class UserPasswordUpdateView(PermissionsMixin, UpdateView):
+ template_name = 'users/user_password_update.html'
+ model = User
+ form_class = forms.UserPasswordForm
+ success_url = reverse_lazy('users:user-profile')
+ permission_classes = [IsValidUser, UserCanUpdatePassword]
+
+ def get_object(self, queryset=None):
+ return self.request.user
+
+ def get_context_data(self, **kwargs):
+ check_rules = get_password_check_rules()
+ context = {
+ 'app': _('Users'),
+ 'action': _('Password update'),
+ 'password_check_rules': check_rules,
+ }
+ kwargs.update(context)
+ return super().get_context_data(**kwargs)
+
+ def get_success_url(self):
+ auth_logout(self.request)
+ return super().get_success_url()
+
+ def form_valid(self, form):
+ password = form.cleaned_data.get('new_password')
+ is_ok = check_password_rules(password)
+ if not is_ok:
+ form.add_error(
+ "new_password",
+ _("* Your password does not meet the requirements")
+ )
+ return self.form_invalid(form)
+ return super().form_valid(form)
+
+
+class UserPublicKeyUpdateView(PermissionsMixin, UpdateView):
+ template_name = 'users/user_pubkey_update.html'
+ model = User
+ form_class = forms.UserPublicKeyForm
+ permission_classes = [IsValidUser, UserCanUpdateSSHKey]
+ success_url = reverse_lazy('users:user-profile')
+
+ def get_object(self, queryset=None):
+ return self.request.user
+
+ def get_context_data(self, **kwargs):
+ context = {
+ 'app': _('Users'),
+ 'action': _('Public key update'),
+ }
+ kwargs.update(context)
+ return super().get_context_data(**kwargs)
+
+
+class UserPublicKeyGenerateView(PermissionsMixin, View):
+ permission_classes = [IsValidUser]
+
+ def get(self, request, *args, **kwargs):
+ private, public = ssh_key_gen(username=request.user.username, hostname='jumpserver')
+ request.user.public_key = public
+ request.user.save()
+ response = HttpResponse(private, content_type='text/plain')
+ filename = "{0}-jumpserver.pem".format(request.user.username)
+ response['Content-Disposition'] = 'attachment; filename={}'.format(filename)
+ return response
+
+
+class UserCheckPasswordView(FormView):
+ template_name = 'users/user_password_check.html'
+ form_class = forms.UserCheckPasswordForm
+
+ def form_valid(self, form):
+ user = get_user_or_tmp_user(self.request)
+ password = form.cleaned_data.get('password')
+ user = authenticate(username=user.username, password=password)
+ if not user:
+ form.add_error("password", _("Password invalid"))
+ return self.form_invalid(form)
+ if not user.mfa_is_otp():
+ user.enable_mfa()
+ user.save()
+ return redirect(self.get_success_url())
+
+ def get_success_url(self):
+ if settings.OTP_IN_RADIUS:
+ success_url = reverse_lazy('users:user-otp-settings-success')
+ else:
+ success_url = reverse('users:user-otp-enable-install-app')
+ return success_url
+
+ def get_context_data(self, **kwargs):
+ context = {
+ 'user': get_user_or_tmp_user(self.request)
+ }
+ kwargs.update(context)
+ return super().get_context_data(**kwargs)
+
+
+class UserOtpEnableInstallAppView(TemplateView):
+ template_name = 'users/user_otp_enable_install_app.html'
+
+ def get_context_data(self, **kwargs):
+ user = get_user_or_tmp_user(self.request)
+ context = {
+ 'user': user
+ }
+ kwargs.update(context)
+ return super().get_context_data(**kwargs)
+
+
+class UserOtpEnableBindView(TemplateView, FormView):
+ template_name = 'users/user_otp_enable_bind.html'
+ form_class = forms.UserCheckOtpCodeForm
+ success_url = reverse_lazy('users:user-otp-settings-success')
+
+ def form_valid(self, form):
+ otp_code = form.cleaned_data.get('otp_code')
+ otp_secret_key = cache.get(self.request.session.session_key+'otp_key', '')
+
+ if check_otp_code(otp_secret_key, otp_code):
+ self.save_otp(otp_secret_key)
+ return super().form_valid(form)
+
+ else:
+ form.add_error("otp_code", _("MFA code invalid, or ntp sync server time"))
+ return self.form_invalid(form)
+
+ def save_otp(self, otp_secret_key):
+ user = get_user_or_tmp_user(self.request)
+ user.enable_mfa()
+ user.otp_secret_key = otp_secret_key
+ user.save()
+
+ def get_context_data(self, **kwargs):
+ user = get_user_or_tmp_user(self.request)
+ otp_uri, otp_secret_key = generate_otp_uri(self.request)
+ context = {
+ 'otp_uri': otp_uri,
+ 'otp_secret_key': otp_secret_key,
+ 'user': user
+ }
+ kwargs.update(context)
+ return super().get_context_data(**kwargs)
+
+
+class UserDisableMFAView(FormView):
+ template_name = 'users/user_disable_mfa.html'
+ form_class = forms.UserCheckOtpCodeForm
+ success_url = reverse_lazy('users:user-otp-settings-success')
+
+ def form_valid(self, form):
+ user = self.request.user
+ otp_code = form.cleaned_data.get('otp_code')
+
+ valid = user.check_mfa(otp_code)
+ if valid:
+ user.disable_mfa()
+ user.save()
+ return super().form_valid(form)
+ else:
+ form.add_error('otp_code', _('MFA code invalid, or ntp sync server time'))
+ return super().form_invalid(form)
+
+
+class UserOtpUpdateView(UserDisableMFAView):
+ success_url = reverse_lazy('users:user-otp-enable-bind')
+
+
+class UserOtpSettingsSuccessView(TemplateView):
+ template_name = 'flash_message_standalone.html'
+
+ def get_context_data(self, **kwargs):
+ title, describe = self.get_title_describe()
+ context = {
+ 'title': title,
+ 'messages': describe,
+ 'interval': 1,
+ 'redirect_url': reverse('authentication:login'),
+ 'auto_redirect': True,
+ }
+ kwargs.update(context)
+ return super().get_context_data(**kwargs)
+
+ def get_title_describe(self):
+ user = get_user_or_tmp_user(self.request)
+ if self.request.user.is_authenticated:
+ auth_logout(self.request)
+ title = _('MFA enable success')
+ describe = _('MFA enable success, return login page')
+ if not user.mfa_enabled:
+ title = _('MFA disable success')
+ describe = _('MFA disable success, return login page')
+ delete_tmp_user_for_cache(self.request)
+ return title, describe
+
diff --git a/apps/users/views/user.py b/apps/users/views/user.py
index 6063c55e9..8e6650240 100644
--- a/apps/users/views/user.py
+++ b/apps/users/views/user.py
@@ -4,48 +4,37 @@ from __future__ import unicode_literals
from django.contrib import messages
-from django.contrib.auth import authenticate
from django.contrib.messages.views import SuccessMessageMixin
from django.core.cache import cache
-from django.conf import settings
-from django.http import HttpResponse
from django.shortcuts import redirect
-from django.urls import reverse_lazy, reverse
+from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
-from django.views import View
from django.views.generic.base import TemplateView
from django.views.generic.edit import (
- CreateView, UpdateView, FormView
+ CreateView, UpdateView
)
from django.views.generic.detail import DetailView
-from django.contrib.auth import logout as auth_logout
from common.const import (
create_success_msg, update_success_msg, KEY_CACHE_RESOURCES_ID
)
-from common.utils import get_logger, ssh_key_gen
+from common.utils import get_logger
from common.permissions import (
- PermissionsMixin, IsOrgAdmin, IsValidUser,
- UserCanUpdatePassword, UserCanUpdateSSHKey,
+ PermissionsMixin, IsOrgAdmin,
CanUpdateDeleteUser,
)
from orgs.utils import current_org
from .. import forms
from ..models import User, UserGroup
-from ..utils import generate_otp_uri, check_otp_code, \
- get_user_or_tmp_user, get_password_check_rules, check_password_rules, \
- is_need_unblock
+from ..utils import get_password_check_rules, is_need_unblock
from ..signals import post_user_create
__all__ = [
'UserListView', 'UserCreateView', 'UserDetailView',
- 'UserUpdateView', 'UserGrantedAssetView', 'UserProfileView',
- 'UserProfileUpdateView', 'UserPasswordUpdateView',
- 'UserPublicKeyUpdateView', 'UserBulkUpdateView',
- 'UserPublicKeyGenerateView',
- 'UserOtpEnableAuthenticationView', 'UserOtpEnableInstallAppView',
- 'UserOtpEnableBindView', 'UserOtpSettingsSuccessView',
- 'UserOtpDisableAuthenticationView', 'UserOtpUpdateView'
+ 'UserUpdateView', 'UserBulkUpdateView',
+ 'UserGrantedAssetView', 'UserAssetPermissionListView',
+ 'UserGrantedRemoteAppView', 'UserRemoteAppPermissionListView',
+ 'UserGrantedDatabasesAppView', 'UserDatabaseAppPermissionListView',
]
logger = get_logger(__name__)
@@ -177,7 +166,7 @@ class UserBulkUpdateView(PermissionsMixin, TemplateView):
class UserDetailView(PermissionsMixin, DetailView):
model = User
template_name = 'users/user_detail.html'
- context_object_name = "user_object"
+ context_object_name = "object"
key_prefix_block = "_LOGIN_BLOCK_{}"
permission_classes = [IsOrgAdmin]
@@ -221,239 +210,71 @@ class UserGrantedAssetView(PermissionsMixin, DetailView):
return super().get_context_data(**kwargs)
-class UserProfileView(PermissionsMixin, TemplateView):
- template_name = 'users/user_profile.html'
- permission_classes = [IsValidUser]
-
- def get_context_data(self, **kwargs):
- mfa_setting = settings.SECURITY_MFA_AUTH
- context = {
- 'action': _('Profile'),
- 'mfa_setting': mfa_setting if mfa_setting is not None else False,
- }
- kwargs.update(context)
- return super().get_context_data(**kwargs)
-
-
-class UserProfileUpdateView(PermissionsMixin, UpdateView):
- template_name = 'users/user_profile_update.html'
+class UserAssetPermissionListView(PermissionsMixin, DetailView):
model = User
- permission_classes = [IsValidUser]
- form_class = forms.UserProfileForm
- success_url = reverse_lazy('users:user-profile')
-
- def get_object(self, queryset=None):
- return self.request.user
-
- def get_context_data(self, **kwargs):
- context = {
- 'app': _('User'),
- 'action': _('Profile setting'),
- }
- kwargs.update(context)
- return super().get_context_data(**kwargs)
-
-
-class UserPasswordUpdateView(PermissionsMixin, UpdateView):
- template_name = 'users/user_password_update.html'
- model = User
- form_class = forms.UserPasswordForm
- success_url = reverse_lazy('users:user-profile')
- permission_classes = [IsValidUser, UserCanUpdatePassword]
-
- def get_object(self, queryset=None):
- return self.request.user
-
- def get_context_data(self, **kwargs):
- check_rules = get_password_check_rules()
- context = {
- 'app': _('Users'),
- 'action': _('Password update'),
- 'password_check_rules': check_rules,
- }
- kwargs.update(context)
- return super().get_context_data(**kwargs)
-
- def get_success_url(self):
- auth_logout(self.request)
- return super().get_success_url()
-
- def form_valid(self, form):
- password = form.cleaned_data.get('new_password')
- is_ok = check_password_rules(password)
- if not is_ok:
- form.add_error(
- "new_password",
- _("* Your password does not meet the requirements")
- )
- return self.form_invalid(form)
- return super().form_valid(form)
-
-
-class UserPublicKeyUpdateView(PermissionsMixin, UpdateView):
- template_name = 'users/user_pubkey_update.html'
- model = User
- form_class = forms.UserPublicKeyForm
- permission_classes = [IsValidUser, UserCanUpdateSSHKey]
- success_url = reverse_lazy('users:user-profile')
-
- def get_object(self, queryset=None):
- return self.request.user
+ template_name = 'users/user_asset_permission.html'
+ permission_classes = [IsOrgAdmin]
def get_context_data(self, **kwargs):
context = {
'app': _('Users'),
- 'action': _('Public key update'),
+ 'action': _('Asset permission'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
-class UserPublicKeyGenerateView(PermissionsMixin, View):
- permission_classes = [IsValidUser]
-
- def get(self, request, *args, **kwargs):
- private, public = ssh_key_gen(username=request.user.username, hostname='jumpserver')
- request.user.public_key = public
- request.user.save()
- response = HttpResponse(private, content_type='text/plain')
- filename = "{0}-jumpserver.pem".format(request.user.username)
- response['Content-Disposition'] = 'attachment; filename={}'.format(filename)
- return response
-
-
-class UserOtpEnableAuthenticationView(FormView):
- template_name = 'users/user_password_authentication.html'
- form_class = forms.UserCheckPasswordForm
-
- def get_form(self, form_class=None):
- user = get_user_or_tmp_user(self.request)
- form = super().get_form(form_class=form_class)
- form['username'].initial = user.username
- return form
+class UserGrantedRemoteAppView(PermissionsMixin, DetailView):
+ model = User
+ template_name = 'users/user_granted_remote_app.html'
+ permission_classes = [IsOrgAdmin]
def get_context_data(self, **kwargs):
- user = get_user_or_tmp_user(self.request)
context = {
- 'user': user
+ 'app': _('Users'),
+ 'action': _('User granted RemoteApp'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
- def form_valid(self, form):
- user = get_user_or_tmp_user(self.request)
- password = form.cleaned_data.get('password')
- user = authenticate(username=user.username, password=password)
- if not user:
- form.add_error("password", _("Password invalid"))
- return self.form_invalid(form)
- if user.mfa_is_otp():
- return redirect(self.get_success_url())
- else:
- user.enable_mfa()
- user.save()
- return redirect('users:user-otp-settings-success')
- def get_success_url(self):
- return reverse('users:user-otp-enable-install-app')
-
-
-class UserOtpEnableInstallAppView(TemplateView):
- template_name = 'users/user_otp_enable_install_app.html'
+class UserRemoteAppPermissionListView(PermissionsMixin, DetailView):
+ model = User
+ template_name = 'users/user_remote_app_permission.html'
+ permission_classes = [IsOrgAdmin]
def get_context_data(self, **kwargs):
- user = get_user_or_tmp_user(self.request)
context = {
- 'user': user
+ 'app': _('Users'),
+ 'action': _('RemoteApp permission'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
-class UserOtpEnableBindView(TemplateView, FormView):
- template_name = 'users/user_otp_enable_bind.html'
- form_class = forms.UserCheckOtpCodeForm
- success_url = reverse_lazy('users:user-otp-settings-success')
+class UserGrantedDatabasesAppView(PermissionsMixin, DetailView):
+ model = User
+ template_name = 'users/user_granted_database_app.html'
+ permission_classes = [IsOrgAdmin]
def get_context_data(self, **kwargs):
- user = get_user_or_tmp_user(self.request)
- otp_uri, otp_secret_key = generate_otp_uri(self.request)
context = {
- 'otp_uri': otp_uri,
- 'otp_secret_key': otp_secret_key,
- 'user': user
+ 'app': _('Users'),
+ 'action': _('User granted DatabaseApp'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
- def form_valid(self, form):
- otp_code = form.cleaned_data.get('otp_code')
- otp_secret_key = cache.get(self.request.session.session_key+'otp_key', '')
- if check_otp_code(otp_secret_key, otp_code):
- self.save_otp(otp_secret_key)
- return super().form_valid(form)
-
- else:
- form.add_error("otp_code", _("MFA code invalid, or ntp sync server time"))
- return self.form_invalid(form)
-
- def save_otp(self, otp_secret_key):
- user = get_user_or_tmp_user(self.request)
- user.enable_mfa()
- user.otp_secret_key = otp_secret_key
- user.save()
-
-
-class UserOtpDisableAuthenticationView(FormView):
- template_name = 'users/user_otp_authentication.html'
- form_class = forms.UserCheckOtpCodeForm
- success_url = reverse_lazy('users:user-otp-settings-success')
-
- def form_valid(self, form):
- user = self.request.user
- otp_code = form.cleaned_data.get('otp_code')
- otp_secret_key = user.otp_secret_key
-
- if check_otp_code(otp_secret_key, otp_code):
- user.disable_mfa()
- user.save()
- return super().form_valid(form)
- else:
- form.add_error('otp_code', _('MFA code invalid, or ntp sync server time'))
- return super().form_invalid(form)
-
-
-class UserOtpUpdateView(UserOtpDisableAuthenticationView):
- success_url = reverse_lazy('users:user-otp-enable-bind')
-
-
-class UserOtpSettingsSuccessView(TemplateView):
- template_name = 'flash_message_standalone.html'
-
- # def get(self, request, *args, **kwargs):
- # return super().get(request, *args, **kwargs)
+class UserDatabaseAppPermissionListView(PermissionsMixin, DetailView):
+ model = User
+ template_name = 'users/user_database_app_permission.html'
+ permission_classes = [IsOrgAdmin]
def get_context_data(self, **kwargs):
- title, describe = self.get_title_describe()
context = {
- 'title': title,
- 'messages': describe,
- 'interval': 1,
- 'redirect_url': reverse('authentication:login'),
- 'auto_redirect': True,
+ 'app': _('Users'),
+ 'action': _('DatabaseApp permission'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
-
- def get_title_describe(self):
- user = get_user_or_tmp_user(self.request)
- if self.request.user.is_authenticated:
- auth_logout(self.request)
- title = _('MFA enable success')
- describe = _('MFA enable success, return login page')
- if not user.mfa_enabled:
- title = _('MFA disable success')
- describe = _('MFA disable success, return login page')
-
- return title, describe
diff --git a/jms b/jms
index 7d0c3d334..fcbe189a8 100755
--- a/jms
+++ b/jms
@@ -19,28 +19,23 @@ from daemon import pidfile
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, BASE_DIR)
+logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
+
try:
from apps.jumpserver import const
__version__ = const.VERSION
except ImportError as e:
- logging.info("Not found __version__: {}".format(e))
- logging.info("Sys path: {}".format(sys.path))
- logging.info("Python is: ")
+ print("Not found __version__: {}".format(e))
+ print("Python is: ")
logging.info(subprocess.call('which python', shell=True))
__version__ = 'Unknown'
- try:
- import apps
- logging.info("List apps: {}".format(os.listdir('apps')))
- logging.info('apps is: {}'.format(apps))
- except:
- pass
+ sys.exit(1)
try:
- from apps.jumpserver.conf import load_user_config
- CONFIG = load_user_config()
+ from apps.jumpserver.const import CONFIG
except ImportError as e:
- logging.info("Import error: {}".format(e))
- logging.info("Could not find config file, `cp config_example.yml config.yml`")
+ print("Import error: {}".format(e))
+ print("Could not find config file, `cp config_example.yml config.yml`")
sys.exit(1)
os.environ["PYTHONIOENCODING"] = "UTF-8"
@@ -445,7 +440,10 @@ def stop_service(srv, sig=15):
if process is None:
print("\033[31m No process found\033[0m")
continue
- process.wait(1)
+ try:
+ process.wait(1)
+ except:
+ pass
for i in range(STOP_TIMEOUT):
if i == STOP_TIMEOUT - 1:
print("\033[31m Error\033[0m")
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
index d99c6ee27..f20d5fdaa 100644
--- a/requirements/requirements.txt
+++ b/requirements/requirements.txt
@@ -89,3 +89,4 @@ flower==0.9.3
channels-redis==2.4.0
channels==2.3.0
daphne==2.3.0
+psutil==5.6.5