Compare commits

...

72 Commits

Author SHA1 Message Date
Jiangjie.Bai
502657bad4 Merge pull request #6294 from jumpserver/dev
v2.11.0 rc5
2021-06-17 12:23:20 +08:00
ibuler
b5120e72c8 perf(notification): 发送html msg 2021-06-17 12:20:51 +08:00
Bai
2ca659414e fix: 修改应用账号序列类添加token字段 2021-06-17 12:13:06 +08:00
Jiangjie.Bai
64f772e747 Merge pull request #6291 from jumpserver/dev
v2.11.0. rc5
2021-06-17 11:49:04 +08:00
Bai
67a897f9c3 fix: 修改ldap导入 2021-06-17 11:48:00 +08:00
Jiangjie.Bai
d0a9ccbdfe Merge pull request #6286 from jumpserver/dev
v2.11.0 rc5 (2)
2021-06-16 19:47:51 +08:00
xinwen
1a30675a86 fix: 去掉命令告警开关 2021-06-16 18:03:17 +08:00
ibuler
f6273450bb perf: 优化批量危险命令告警 2021-06-16 18:02:17 +08:00
ibuler
8f35fcd6f9 perf: 优化通知迁移 2021-06-16 16:58:07 +08:00
xinwen
1999cfdfeb perf: 优化钉钉命令告警 2021-06-16 16:57:28 +08:00
Bai
c4af78c9f0 fix: 修改AuthBook删除raise异常类 2021-06-16 14:42:03 +08:00
Bai
a3d02decd6 fix: 修改翻译 2021-06-16 14:28:28 +08:00
ibuler
e623f63fcf perf: 修改i18n
perf: 优化命令告警,优化翻译
2021-06-16 14:24:57 +08:00
Jiangjie.Bai
4f1b2aceda Merge pull request #6277 from jumpserver/dev
v2.11.0 rc5
2021-06-16 13:03:17 +08:00
健健
94fc1fb53b fix: 导入数据解析 title 时,没有过滤 read only 字段 (#6269)
* feat: Update README (#6182)

* feat: Update README

* feat: Update README

* Update README.md

* feat: update README

* fix: 导入数据解析 title 时,没有过滤 read only 字段

type,type_display 翻译都是一样的,导出时使用的是 type,导入时识别成 type_display

Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
2021-06-16 13:00:55 +08:00
xinwen
937acbd0b5 fix: 资产授权不能打开或关闭 2021-06-16 12:54:03 +08:00
xinwen
067a70463e fix: 高危命令邮件收不到 2021-06-16 12:41:14 +08:00
Bai
b115ed3b79 fix: 修改LDAP用户导入默认添加到Default组织 2021-06-16 11:13:21 +08:00
Jiangjie.Bai
057fbdf0b1 Merge pull request #6268 from jumpserver/dev
v2.11.0 rc3
2021-06-15 14:50:02 +08:00
xinwen
5263a146e2 fix: 修复站内信迁移脚本问题 2021-06-15 14:47:16 +08:00
Jiangjie.Bai
84070a558e Merge pull request #6265 from jumpserver/dev
v2.11.0 rc2
2021-06-15 10:49:46 +08:00
Bai
e0604a3211 feat: 修改翻译 2021-06-15 10:42:06 +08:00
Bai
00e4c3cd07 feat: 添加应用用户API 2021-06-15 10:42:06 +08:00
ibuler
97a0e27307 perf: 优化消息中心未读数量 2021-06-11 18:02:53 +08:00
ibuler
8d3c1bd783 perf: 优化获取token secret, 重新校验权限 2021-06-10 19:51:11 +08:00
ibuler
db99ab80db perf(auth): 授权token形式登录,支持记录登录日志 2021-06-10 18:07:24 +08:00
Jiangjie.Bai
1e8d9ba2ec Merge pull request #6256 from jumpserver/dev
v2.11.0 rc1
2021-06-10 14:03:45 +08:00
xinwen
7dddf0c3c2 fix: 站内信未读信息计数不准 2021-06-10 10:24:54 +08:00
fit2bot
891a5157a7 perf: 优化token时间 (#6252)
* perf: 修复上次引起的小bug

* perf: 优化token时间

Co-authored-by: ibuler <ibuler@qq.com>
2021-06-09 20:10:34 +08:00
ibuler
34b2a5fe0b perf: 修复上次引起的小bug 2021-06-09 15:47:26 +08:00
ibuler
de6908e5a6 perf: rdp file添加domain
fix: 禁用的用户不返回信息

perf: 优化token,禁用的资产无法链接
2021-06-09 14:18:15 +08:00
fit2bot
d6527e3b02 perf: 优化支持记录密码 (#6247)
* perf: 优化 xrdp setting

* perf: 优化支持记录密码

Co-authored-by: ibuler <ibuler@qq.com>
2021-06-08 20:50:15 +08:00
ibuler
33a29ae788 perf: 优化 xrdp setting 2021-06-08 15:25:32 +08:00
ibuler
a2eb431015 perf: 优化自动分辨率 2021-06-08 12:48:48 +08:00
Bai
8fbea2f702 fix: 修改资产账号name为非必填 2021-06-08 11:38:37 +08:00
fit2bot
af92271a52 feat: 调整站内信接口 (#6228)
* feat: 调整站内信接口

* 添加 websockt

* 添加信息类型字段

* 添加 has_read 过滤参数

* feat: 调整站内信接口

* 添加 websockt

* 添加信息类型字段

* 添加 has_read 过滤参数

* 去掉type websocket

* perf: 去掉type

Co-authored-by: xinwen <coderWen@126.com>
Co-authored-by: ibuler <ibuler@qq.com>
2021-06-08 11:11:27 +08:00
ibuler
391a5cb7d0 perf: 修复手动设置密码的问题 2021-06-07 10:46:58 +08:00
xinwen
daf7d98f0e fix: 其他组织中创建的用户不要添加到默认组织了 2021-06-04 04:30:40 -05:00
Jiangjie.Bai
ed297fd1bd Merge pull request #6226 from jumpserver/pr@dev@add_missing_migrations
chore: 添加删除的文件
2021-06-04 14:40:32 +08:00
Bai
f91bef4105 feat: 修改依赖包版本号: jms-storage-sdk==0.0.37 2021-06-04 14:37:02 +08:00
Bai
a8d84fc6e1 feat: 修改迁移文件 2021-06-04 11:39:17 +08:00
Bai
0c7838d0e3 feat: 修改迁移文件 2021-06-04 11:39:17 +08:00
Jiangjie.Bai
f26483c9cd Merge pull request #6224 from jumpserver/feat_account_manager
feat: 添加账号管理相关API
2021-06-04 11:15:53 +08:00
Bai
5daca6592b feat: 修改文案 后端 -> 来源 2021-06-04 11:14:53 +08:00
Bai
0bced39f08 fix: 修复redis服务异常时(如: 主从切换), 用户session立即过期的问题 2021-06-03 22:04:10 -05:00
ibuler
6d83dd0e3a chore: 添加删除的文件 2021-06-03 14:54:41 +08:00
ibuler
46e99d10cb Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2021-06-03 14:28:39 +08:00
liubo
95eb11422a feat: 支持添加 OBS 存储 2021-06-03 01:28:23 -05:00
ibuler
e8b3ee4565 perf: 优化系统用户,支持用户设置临时密码
perf: 优化rdp file下载

perf: 修改密码途观选项

perf: 优化api获取
2021-06-03 01:24:28 -05:00
Bai
1e99be1775 feat: 修改获取应用用户API 2021-06-03 13:59:44 +08:00
Bai
adae509bc0 fix: 修复组织批量删除的问题 2021-06-03 11:36:24 +08:00
ibuler
7868e91844 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2021-06-03 11:19:02 +08:00
xinwen
a9bdbcf7c6 fix: metadata api view 报错 2021-06-02 22:02:14 -05:00
xinwen
a809eac2b8 fix: 修复获取 Metadata 时,获取的总是 action 为 metadata 2021-06-02 04:41:01 -05:00
Bai
bdab93260f feat: 资产用户API返回 BackendDisplay 和 Name 字段 2021-06-02 17:00:31 +08:00
fit2bot
4ef3b2630a feat: 站内信 (#6183)
* 添加站内信

* s

* s

* 添加接口

* fix

* fix

* 重构了一些

* 完成

* 完善

* s

* s

* s

* s

* s

* s

* 测试ok

* 替换业务中发送消息的方式

* 修改

* s

* 去掉 update 兼容 create

* 添加 unread total 接口

* 调整json字段

Co-authored-by: xinwen <coderWen@126.com>
2021-05-31 17:20:38 +08:00
Bai
4eef25982d feat: 更新 ApplicationUserList API 2021-05-27 18:42:43 +08:00
xinwen
b82e9f860b fix: users 遗漏一个 migration 2021-05-26 15:26:56 +08:00
Bai
6b46f5b48e feat: 添加ApplicationUserList API 2021-05-24 19:11:47 +08:00
Jiangjie.Bai
fe717f0244 feat: Update README (#6182)
* feat: Update README

* feat: Update README

* Update README.md

* feat: update README
2021-05-24 16:04:40 +08:00
ibuler
33fb063f78 perf: 暂时禁用xrdp实时监控 2021-05-24 15:37:21 +08:00
老广
7edc9c37f8 Update README.md 2021-05-24 11:02:18 +08:00
Michael Bai
f8b4259a8c fix: 修复创建/更新用户时密码策略相关的问题 2021-05-23 21:56:37 -05:00
Michael Bai
572d0e3f27 fix: 修复parser没有处理int类型数据的问题 2021-05-23 21:54:14 -05:00
ibuler
b334f3c2d9 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2021-05-24 10:46:43 +08:00
Jiangjie.Bai
6b4b9f4b02 Merge pull request #6169 from jumpserver/dev
v2.10.1
2021-05-21 15:20:25 +08:00
ibuler
d765e61991 fix(assets): 修复网关信息没有密码的bug 2021-05-21 15:17:29 +08:00
Bai
9ccde03656 fix: 修改cloud翻译 2021-05-20 22:27:29 -05:00
xinwen
c66f366446 fix: 修复 default 组织用户数量统计错误 2021-05-21 10:36:27 +08:00
ibuler
34d46897f8 fix: 修复周期监测任务配置的bug 2021-05-21 10:35:39 +08:00
ibuler
2d9ce16601 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2021-05-20 15:53:16 +08:00
ibuler
bc4258256a perf: 修改readme 2021-05-20 13:06:37 +08:00
109 changed files with 2701 additions and 605 deletions

1
.gitignore vendored
View File

@@ -15,6 +15,7 @@ dump.rdb
.tox
.cache/
.idea/
.vscode/
db.sqlite3
config.py
config.yml

View File

@@ -37,8 +37,8 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
<table>
<tr>
<td rowspan="8">身份认证<br>Authentication</td>
<td rowspan="5">登录认证</td>
<td rowspan="11">身份认证<br>Authentication</td>
<td rowspan="7">登录认证</td>
<td>资源统一登录与认证</td>
</tr>
<tr>
@@ -53,6 +53,12 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
<tr>
<td>CAS 认证 (实现单点登录)</td>
</tr>
<tr>
<td>钉钉认证 (扫码登录)</td>
</tr>
<tr>
<td>企业微信认证 (扫码登录)</td>
</tr>
<tr>
<td rowspan="2">MFA认证</td>
<td>MFA 二次认证Google Authenticator</td>
@@ -64,6 +70,10 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
<td>登录复核</td>
<td>用户登录行为受管理员的监管与控制:small_orange_diamond:</td>
</tr>
<tr>
<td>登录限制</td>
<td>用户登录来源 IP 受管理员控制(支持黑/白名单)</td>
</tr>
<tr>
<td rowspan="11">账号管理<br>Account</td>
<td rowspan="2">集中账号</td>
@@ -105,7 +115,7 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
<td>统一对资产主机的用户密码进行查看、更新、测试操作:small_orange_diamond:</td>
</tr>
<tr>
<td rowspan="15">授权控制<br>Authorization</td>
<td rowspan="17">授权控制<br>Authorization</td>
<td>多维授权</td>
<td>对用户、用户组、资产、资产节点、应用以及系统用户进行授权</td>
</tr>
@@ -157,17 +167,27 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
<td>工单管理</td>
<td>支持对用户登录请求行为进行控制:small_orange_diamond:</td>
</tr>
<tr>
<td rowspan="2">访问控制</td>
<td>登录资产复核(通过 SSH/Telnet 协议登录资产):small_orange_diamond:</td>
</tr>
<tr>
<td>命令执行复核:small_orange_diamond:</td>
</tr>
<tr>
<td>组织管理</td>
<td>实现多租户管理与权限隔离:small_orange_diamond:</td>
</tr>
<tr>
<td rowspan="7">安全审计<br>Audit</td>
<td rowspan="8">安全审计<br>Audit</td>
<td>操作审计</td>
<td>用户操作行为审计</td>
</tr>
<tr>
<td rowspan="2">会话审计</td>
<td rowspan="3">会话审计</td>
<td>在线会话内容监控</td>
</tr>
<tr>
<td>在线会话内容审计</td>
</tr>
<tr>
@@ -245,6 +265,7 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
- [极速安装](https://docs.jumpserver.org/zh/master/install/setup_by_fast/)
- [完整文档](https://docs.jumpserver.org)
- [演示视频](https://www.bilibili.com/video/BV1ZV41127GB)
- [手动安装](https://github.com/jumpserver/installer)
## 组件项目
- [Lina](https://github.com/jumpserver/lina) JumpServer Web UI 项目
@@ -300,3 +321,4 @@ Licensed under The GNU General Public License version 2 (GPLv2) (the "License")
https://www.gnu.org/licenses/gpl-2.0.html
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

View File

@@ -1,3 +1,4 @@
from .application import *
from .application_user import *
from .mixin import *
from .remote_app import *

View File

@@ -4,16 +4,16 @@
from orgs.mixins.api import OrgBulkModelViewSet
from ..hands import IsOrgAdminOrAppUser
from .. import models, serializers
from .. import serializers
from ..models import Application
__all__ = ['ApplicationViewSet']
class ApplicationViewSet(OrgBulkModelViewSet):
model = models.Application
model = Application
filterset_fields = ('name', 'type', 'category')
search_fields = filterset_fields
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.ApplicationSerializer

View File

@@ -0,0 +1,55 @@
# coding: utf-8
#
from rest_framework import generics
from django.conf import settings
from ..hands import IsOrgAdminOrAppUser, IsOrgAdmin, NeedMFAVerify
from .. import serializers
from ..models import Application, ApplicationUser
from perms.models import ApplicationPermission
class ApplicationUserListApi(generics.ListAPIView):
permission_classes = (IsOrgAdmin, )
filterset_fields = ('name', 'username')
search_fields = filterset_fields
serializer_class = serializers.ApplicationUserSerializer
_application = None
@property
def application(self):
if self._application is None:
app_id = self.request.query_params.get('application_id')
if app_id:
self._application = Application.objects.get(id=app_id)
return self._application
def get_serializer_context(self):
context = super().get_serializer_context()
context.update({
'application': self.application
})
return context
def get_queryset(self):
queryset = ApplicationUser.objects.none()
if not self.application:
return queryset
system_user_ids = ApplicationPermission.objects.filter(applications=self.application)\
.values_list('system_users', flat=True)
if not system_user_ids:
return queryset
queryset = ApplicationUser.objects.filter(id__in=system_user_ids)
return queryset
class ApplicationUserAuthInfoListApi(ApplicationUserListApi):
serializer_class = serializers.ApplicationUserWithAuthInfoSerializer
http_method_names = ['get']
permission_classes = [IsOrgAdminOrAppUser]
def get_permissions(self):
if settings.SECURITY_VIEW_AUTH_NEED_MFA:
self.permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
return super().get_permissions()

View File

@@ -11,5 +11,5 @@
"""
from common.permissions import IsAppUser, IsOrgAdmin, IsValidUser, IsOrgAdminOrAppUser
from common.permissions import IsAppUser, IsOrgAdmin, IsValidUser, IsOrgAdminOrAppUser, NeedMFAVerify
from users.models import User, UserGroup

View File

@@ -3,7 +3,7 @@ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.models import OrgModelMixin
from common.mixins import CommonModelMixin
from assets.models import Asset
from assets.models import Asset, SystemUser
from .. import const
@@ -68,3 +68,8 @@ class Application(CommonModelMixin, OrgModelMixin):
raise ValueError("Remote App not has asset attr")
asset = Asset.objects.filter(id=asset_id).first()
return asset
class ApplicationUser(SystemUser):
class Meta:
proxy = True

View File

@@ -6,11 +6,12 @@ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.drf.serializers import MethodSerializer
from .attrs import category_serializer_classes_mapping, type_serializer_classes_mapping
from assets.serializers import SystemUserSerializer
from .. import models
__all__ = [
'ApplicationSerializer', 'ApplicationSerializerMixin',
'ApplicationUserSerializer', 'ApplicationUserWithAuthInfoSerializer'
]
@@ -66,3 +67,42 @@ class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSeri
_attrs.update(attrs)
return _attrs
class ApplicationUserSerializer(SystemUserSerializer):
application_name = serializers.SerializerMethodField(label=_('Application name'))
application_category = serializers.SerializerMethodField(label=_('Application category'))
application_type = serializers.SerializerMethodField(label=_('Application type'))
class Meta(SystemUserSerializer.Meta):
model = models.ApplicationUser
fields_mini = [
'id', 'application_name', 'application_category', 'application_type', 'name', 'username'
]
fields_small = fields_mini + [
'protocol', 'login_mode', 'login_mode_display', 'priority',
"username_same_with_user", 'comment',
]
fields = fields_small
extra_kwargs = {
'login_mode_display': {'label': _('Login mode display')},
'created_by': {'read_only': True},
}
@property
def application(self):
return self.context['application']
def get_application_name(self, obj):
return self.application.name
def get_application_category(self, obj):
return self.application.get_category_display()
def get_application_type(self, obj):
return self.application.get_type_display()
class ApplicationUserWithAuthInfoSerializer(ApplicationUserSerializer):
class Meta(ApplicationUserSerializer.Meta):
fields = ApplicationUserSerializer.Meta.fields + ['password', 'token']

View File

@@ -14,6 +14,8 @@ router.register(r'applications', api.ApplicationViewSet, 'application')
urlpatterns = [
path('remote-apps/<uuid:pk>/connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'),
path('application-users/', api.ApplicationUserListApi.as_view(), name='application-user'),
path('application-user-auth-infos/', api.ApplicationUserAuthInfoListApi.as_view(), name='application-user-auth-info')
]

View File

@@ -3,14 +3,13 @@ from django.shortcuts import get_object_or_404
from rest_framework.response import Response
from common.utils import get_logger
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from common.drf.filters import CustomFilter
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsValidUser
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics
from orgs.utils import tmp_to_org
from orgs.utils import tmp_to_root_org
from ..models import SystemUser, Asset
from .. import serializers
from ..serializers import SystemUserWithAuthInfoSerializer
from ..serializers import SystemUserWithAuthInfoSerializer, SystemUserTempAuthSerializer
from ..tasks import (
push_system_user_to_assets_manual, test_system_user_connectivity_manual,
push_system_user_to_assets
@@ -21,6 +20,7 @@ logger = get_logger(__file__)
__all__ = [
'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi',
'SystemUserCommandFilterRuleListApi', 'SystemUserTaskApi', 'SystemUserAssetsListView',
'SystemUserTempAuthInfoApi', 'SystemUserAppAuthInfoApi',
]
@@ -57,6 +57,25 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
return Response(status=204)
class SystemUserTempAuthInfoApi(generics.CreateAPIView):
model = SystemUser
permission_classes = (IsValidUser,)
serializer_class = SystemUserTempAuthSerializer
def create(self, request, *args, **kwargs):
serializer = super().get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
pk = kwargs.get('pk')
user = self.request.user
data = serializer.validated_data
instance_id = data.get('instance_id')
with tmp_to_root_org():
instance = get_object_or_404(SystemUser, pk=pk)
instance.set_temp_auth(instance_id, user, data)
return Response(serializer.data, status=201)
class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView):
"""
Get system user with asset auth info
@@ -65,22 +84,30 @@ class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView):
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = SystemUserWithAuthInfoSerializer
def get_exception_handler(self):
def handler(e, context):
return Response({"error": str(e)}, status=400)
return handler
def get_object(self):
instance = super().get_object()
asset_id = self.kwargs.get('asset_id')
user_id = self.request.query_params.get("user_id")
username = self.request.query_params.get("username")
instance.load_asset_more_auth(asset_id=asset_id, user_id=user_id, username=username)
return instance
class SystemUserAppAuthInfoApi(generics.RetrieveAPIView):
"""
Get system user with asset auth info
"""
model = SystemUser
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = SystemUserWithAuthInfoSerializer
def get_object(self):
instance = super().get_object()
username = instance.username
if instance.username_same_with_user:
username = self.request.query_params.get("username")
asset_id = self.kwargs.get('aid')
asset = get_object_or_404(Asset, pk=asset_id)
with tmp_to_org(asset.org_id):
instance.load_asset_special_auth(asset=asset, username=username)
return instance
app_id = self.kwargs.get('app_id')
user_id = self.request.query_params.get("user_id")
if user_id:
instance.load_app_more_auth(app_id, user_id)
return instance
class SystemUserTaskApi(generics.CreateAPIView):

View File

@@ -31,11 +31,11 @@ class BaseBackend:
def qs_to_values(qs):
values = qs.values(
'hostname', 'ip', "asset_id",
'username', 'password', 'private_key', 'public_key',
'name', 'username', 'password', 'private_key', 'public_key',
'score', 'version',
"asset_username", "union_id",
'date_created', 'date_updated',
'org_id', 'backend',
'org_id', 'backend', 'backend_display'
)
return values

View File

@@ -4,6 +4,7 @@ from django.utils.translation import ugettext as _
from functools import reduce
from django.db.models import F, CharField, Value, IntegerField, Q, Count
from django.db.models.functions import Concat
from rest_framework.exceptions import PermissionDenied
from common.utils import get_object_or_none
from orgs.utils import current_org
@@ -106,6 +107,7 @@ class DBBackend(BaseBackend):
class SystemUserBackend(DBBackend):
model = SystemUser.assets.through
backend = 'system_user'
backend_display = _('System user')
prefer = backend
base_score = 0
union_id_length = 2
@@ -138,6 +140,7 @@ class SystemUserBackend(DBBackend):
kwargs = dict(
hostname=F("asset__hostname"),
ip=F("asset__ip"),
name=F("systemuser__name"),
username=F("systemuser__username"),
password=F("systemuser__password"),
private_key=F("systemuser__private_key"),
@@ -152,7 +155,8 @@ class SystemUserBackend(DBBackend):
union_id=Concat(F("systemuser_id"), Value("_"), F("asset_id"),
output_field=CharField()),
org_id=F("asset__org_id"),
backend=Value(self.backend, CharField())
backend=Value(self.backend, CharField()),
backend_display=Value(self.backend_display, CharField()),
)
return kwargs
@@ -174,12 +178,17 @@ class SystemUserBackend(DBBackend):
class DynamicSystemUserBackend(SystemUserBackend):
backend = 'system_user_dynamic'
backend_display = _('System user(Dynamic)')
prefer = 'system_user'
union_id_length = 3
def get_annotate(self):
kwargs = super().get_annotate()
kwargs.update(dict(
name=Concat(
F("systemuser__users__name"), Value('('), F("systemuser__name"), Value(')'),
output_field=CharField()
),
username=F("systemuser__users__username"),
asset_username=Concat(
F("asset__id"), Value("_"),
@@ -221,6 +230,7 @@ class DynamicSystemUserBackend(SystemUserBackend):
class AdminUserBackend(DBBackend):
model = Asset
backend = 'admin_user'
backend_display = _('Admin user')
prefer = backend
base_score = 200
@@ -241,11 +251,12 @@ class AdminUserBackend(DBBackend):
)
def _perform_delete_by_union_id(self, union_id_cleaned):
raise PermissionError(_("Could not remove asset admin user"))
raise PermissionDenied(_("Could not remove asset admin user"))
def all(self):
qs = self.model.objects.all().annotate(
asset_id=F("id"),
name=F("admin_user__name"),
username=F("admin_user__username"),
password=F("admin_user__password"),
private_key=F("admin_user__private_key"),
@@ -256,6 +267,7 @@ class AdminUserBackend(DBBackend):
asset_username=Concat(F("id"), Value("_"), F("admin_user__username"), output_field=CharField()),
union_id=Concat(F("admin_user_id"), Value("_"), F("id"), output_field=CharField()),
backend=Value(self.backend, CharField()),
backend_display=Value(self.backend_display, CharField()),
)
qs = self.qs_to_values(qs)
return qs
@@ -264,6 +276,7 @@ class AdminUserBackend(DBBackend):
class AuthbookBackend(DBBackend):
model = AuthBook
backend = 'db'
backend_display = _('Database')
prefer = backend
base_score = 400
@@ -302,7 +315,7 @@ class AuthbookBackend(DBBackend):
authbook_id, asset_id = union_id_cleaned
authbook = get_object_or_none(AuthBook, pk=authbook_id)
if authbook.is_latest:
raise PermissionError(_("Latest version could not be delete"))
raise PermissionDenied(_("Latest version could not be delete"))
AuthBook.objects.filter(id=authbook_id).delete()
def all(self):
@@ -313,6 +326,7 @@ class AuthbookBackend(DBBackend):
asset_username=Concat(F("asset__id"), Value("_"), F("username"), output_field=CharField()),
union_id=Concat(F("id"), Value("_"), F("asset_id"), output_field=CharField()),
backend=Value(self.backend, CharField()),
backend_display=Value(self.backend_display, CharField()),
)
qs = self.qs_to_values(qs)
return qs

View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-05 10:07
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='adminuser',
options={'ordering': ['name'], 'verbose_name': 'Admin user'},
),
migrations.AlterModelOptions(
name='asset',
options={'verbose_name': 'Asset'},
),
migrations.AlterModelOptions(
name='assetgroup',
options={'ordering': ['name'], 'verbose_name': 'Asset group'},
),
migrations.AlterModelOptions(
name='cluster',
options={'ordering': ['name'], 'verbose_name': 'Cluster'},
),
migrations.AlterModelOptions(
name='systemuser',
options={'ordering': ['name'], 'verbose_name': 'System user'},
),
]

View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-09 15:31
from __future__ import unicode_literals
import assets.models.asset
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0002_auto_20180105_1807'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='cluster',
field=models.ForeignKey(default=assets.models.asset.default_cluster, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='assets', to='assets.Cluster', verbose_name='Cluster'),
),
]

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-25 04:18
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0003_auto_20180109_2331'),
]
operations = [
migrations.AlterField(
model_name='assetgroup',
name='created_by',
field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by'),
),
]

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-26 08:37
from __future__ import unicode_literals
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('assets', '0004_auto_20180125_1218'),
]
operations = [
migrations.CreateModel(
name='Label',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('value', models.CharField(max_length=128, verbose_name='Value')),
('category', models.CharField(choices=[('S', 'System'), ('U', 'User')], default='U', max_length=128, verbose_name='Category')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('comment', models.TextField(blank=True, null=True, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
],
options={
'db_table': 'assets_label',
},
),
migrations.AlterUniqueTogether(
name='label',
unique_together=set([('name', 'value')]),
),
migrations.AddField(
model_name='asset',
name='labels',
field=models.ManyToManyField(blank=True, related_name='assets', to='assets.Label', verbose_name='Labels'),
),
]

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-30 07:02
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0005_auto_20180126_1637'),
]
operations = [
migrations.RemoveField(
model_name='asset',
name='cabinet_no',
),
migrations.RemoveField(
model_name='asset',
name='cabinet_pos',
),
migrations.RemoveField(
model_name='asset',
name='env',
),
migrations.RemoveField(
model_name='asset',
name='remote_card_ip',
),
migrations.RemoveField(
model_name='asset',
name='status',
),
migrations.RemoveField(
model_name='asset',
name='type',
),
]

View File

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-02-25 10:15
from __future__ import unicode_literals
import assets.models.asset
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('assets', '0006_auto_20180130_1502'),
]
operations = [
migrations.CreateModel(
name='Node',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('key', models.CharField(max_length=64, unique=True, verbose_name='Key')),
('value', models.CharField(max_length=128, unique=True, verbose_name='Value')),
('child_mark', models.IntegerField(default=0)),
('date_create', models.DateTimeField(auto_now_add=True)),
],
),
migrations.RemoveField(
model_name='asset',
name='cluster',
),
migrations.RemoveField(
model_name='asset',
name='groups',
),
migrations.RemoveField(
model_name='systemuser',
name='cluster',
),
migrations.AlterField(
model_name='asset',
name='admin_user',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='assets.AdminUser', verbose_name='Admin user'),
),
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp')], default='ssh', max_length=16, verbose_name='Protocol'),
),
migrations.AddField(
model_name='asset',
name='nodes',
field=models.ManyToManyField(default=assets.models.asset.default_node, related_name='assets', to='assets.Node', verbose_name='Nodes'),
),
migrations.AddField(
model_name='systemuser',
name='nodes',
field=models.ManyToManyField(blank=True, to='assets.Node', verbose_name='Nodes'),
),
]

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-03-06 10:04
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0007_auto_20180225_1815'),
]
operations = [
migrations.AlterField(
model_name='adminuser',
name='created_by',
field=models.CharField(max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(max_length=128, verbose_name='Username'),
),
migrations.AlterField(
model_name='asset',
name='platform',
field=models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Other', 'Other')], default='Linux', max_length=128, verbose_name='Platform'),
),
migrations.AlterField(
model_name='systemuser',
name='created_by',
field=models.CharField(max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(max_length=128, verbose_name='Username'),
),
]

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-03-07 04:12
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0008_auto_20180306_1804'),
]
operations = [
migrations.AlterField(
model_name='node',
name='value',
field=models.CharField(max_length=128, verbose_name='Value'),
),
]

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-03-07 09:49
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0009_auto_20180307_1212'),
]
operations = [
migrations.AlterField(
model_name='node',
name='value',
field=models.CharField(max_length=128, unique=True, verbose_name='Value'),
),
]

View File

@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-03-26 01:57
from __future__ import unicode_literals
import assets.models.utils
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('assets', '0010_auto_20180307_1749'),
]
operations = [
migrations.CreateModel(
name='Domain',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
],
),
migrations.CreateModel(
name='Gateway',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
('username', models.CharField(max_length=128, verbose_name='Username')),
('_password', models.CharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('_private_key', models.TextField(blank=True, max_length=4096, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key')),
('_public_key', models.TextField(blank=True, max_length=4096, verbose_name='SSH public key')),
('date_created', models.DateTimeField(auto_now_add=True)),
('date_updated', models.DateTimeField(auto_now=True)),
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
('ip', models.GenericIPAddressField(db_index=True, verbose_name='IP')),
('port', models.IntegerField(default=22, verbose_name='Port')),
('protocol', models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp')], default='ssh', max_length=16, verbose_name='Protocol')),
('comment', models.CharField(blank=True, max_length=128, null=True, verbose_name='Comment')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Domain', verbose_name='Domain')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='asset',
name='domain',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assets', to='assets.Domain', verbose_name='Domain'),
),
]

View File

@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-04 05:02
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0011_auto_20180326_0957'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='domain',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assets', to='assets.Domain', verbose_name='Domain'),
),
]

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-11 03:35
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0012_auto_20180404_1302'),
]
operations = [
migrations.AddField(
model_name='systemuser',
name='assets',
field=models.ManyToManyField(blank=True, to='assets.Asset', verbose_name='Assets'),
),
migrations.AlterField(
model_name='systemuser',
name='sudo',
field=models.TextField(default='/bin/whoami', verbose_name='Sudo'),
),
]

View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-27 04:45
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0013_auto_20180411_1135'),
]
operations = [
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='gateway',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'),
),
]

View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-05-10 04:35
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0014_auto_20180427_1245'),
]
operations = [
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='gateway',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
]

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-05-11 04:03
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0015_auto_20180510_1235'),
]
operations = [
migrations.AlterField(
model_name='node',
name='value',
field=models.CharField(max_length=128, verbose_name='Value'),
),
]

View File

@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-07-02 06:15
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
def migrate_win_to_ssh_protocol(apps, schema_editor):
asset_model = apps.get_model("assets", "Asset")
db_alias = schema_editor.connection.alias
asset_model.objects.using(db_alias).filter(platform__startswith='Win').update(protocol='rdp')
class Migration(migrations.Migration):
dependencies = [
('assets', '0016_auto_20180511_1203'),
]
operations = [
migrations.AddField(
model_name='asset',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)')], default='ssh', max_length=128, verbose_name='Protocol'),
),
migrations.AddField(
model_name='systemuser',
name='login_mode',
field=models.CharField(choices=[('auto', 'Automatic login'), ('manual', 'Manually login')], default='auto', max_length=10, verbose_name='Login mode'),
),
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='asset',
name='platform',
field=models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Windows2016', 'Windows(2016)'), ('Other', 'Other')], default='Linux', max_length=128, verbose_name='Platform'),
),
migrations.AlterField(
model_name='gateway',
name='username',
field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)')], default='ssh', max_length=16, verbose_name='Protocol'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.RunPython(migrate_win_to_ssh_protocol),
]

View File

@@ -0,0 +1,84 @@
# Generated by Django 2.0.7 on 2018-08-07 03:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0017_auto_20180702_1415'),
]
operations = [
migrations.AddField(
model_name='adminuser',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='asset',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='domain',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='gateway',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='label',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='node',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='systemuser',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AlterField(
model_name='adminuser',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterField(
model_name='asset',
name='hostname',
field=models.CharField(max_length=128, verbose_name='Hostname'),
),
migrations.AlterField(
model_name='gateway',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterField(
model_name='systemuser',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterUniqueTogether(
name='adminuser',
unique_together={('name', 'org_id')},
),
migrations.AlterUniqueTogether(
name='asset',
unique_together={('org_id', 'hostname')},
),
migrations.AlterUniqueTogether(
name='gateway',
unique_together={('name', 'org_id')},
),
migrations.AlterUniqueTogether(
name='systemuser',
unique_together={('name', 'org_id')},
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 2.0.7 on 2018-08-16 05:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0018_auto_20180807_1116'),
]
operations = [
migrations.AddField(
model_name='asset',
name='cpu_vcpus',
field=models.IntegerField(null=True, verbose_name='CPU vcpus'),
),
migrations.AlterUniqueTogether(
name='label',
unique_together={('name', 'value', 'org_id')},
),
]

View File

@@ -7,6 +7,7 @@ class AssetUser(AuthBook):
hostname = ""
ip = ""
backend = ""
backend_display = ""
union_id = ""
asset_username = ""

View File

@@ -4,6 +4,7 @@
from django.db import models, transaction
from django.db.models import Max
from django.utils.translation import ugettext_lazy as _
from rest_framework.exceptions import PermissionDenied
from orgs.mixins.models import OrgManager
from .base import BaseUser
@@ -14,7 +15,7 @@ __all__ = ['AuthBook']
class AuthBookQuerySet(models.QuerySet):
def delete(self):
if self.count() > 1:
raise PermissionError(_("Bulk delete deny"))
raise PermissionDenied(_("Bulk delete deny"))
return super().delete()

View File

@@ -11,8 +11,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from common.db.models import ChoiceSet
from common.utils import random_string
from common.utils import random_string, signer
from common.utils import (
ssh_key_string_to_obj, ssh_key_gen, get_logger, lazyproperty
)

View File

@@ -7,9 +7,10 @@ import logging
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.cache import cache
from common.utils import signer
from common.fields.model import JsonListCharField
from common.utils import signer, get_object_or_none
from common.exceptions import JMSException
from .base import BaseUser
from .asset import Asset
@@ -185,6 +186,81 @@ class SystemUser(BaseUser):
if self.username_same_with_user:
self.username = other.username
def set_temp_auth(self, asset_or_app_id, user_id, auth, ttl=300):
if not auth:
raise ValueError('Auth not set')
key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id)
logger.debug(f'Set system user temp auth: {key}')
cache.set(key, auth, ttl)
def get_temp_auth(self, asset_or_app_id, user_id):
key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id)
logger.debug(f'Get system user temp auth: {key}')
password = cache.get(key)
return password
def load_tmp_auth_if_has(self, asset_or_app_id, user):
if not asset_or_app_id or not user:
return
if self.login_mode != self.LOGIN_MANUAL:
pass
auth = self.get_temp_auth(asset_or_app_id, user)
if not auth:
return
username = auth.get('username')
password = auth.get('password')
if username:
self.username = username
if password:
self.password = password
def load_app_more_auth(self, app_id=None, user_id=None):
from users.models import User
if self.login_mode == self.LOGIN_MANUAL:
self.password = ''
self.private_key = ''
if not user_id:
return
user = get_object_or_none(User, pk=user_id)
if not user:
return
self.load_tmp_auth_if_has(app_id, user)
def load_asset_more_auth(self, asset_id=None, username=None, user_id=None):
from users.models import User
if self.login_mode == self.LOGIN_MANUAL:
self.password = ''
self.private_key = ''
asset = None
if asset_id:
asset = get_object_or_none(Asset, pk=asset_id)
# 没有资产就没有必要继续了
if not asset:
logger.debug('Asset not found, pass')
return
user = None
if user_id:
user = get_object_or_none(User, pk=user_id)
if self.username_same_with_user:
if user and not username:
username = user.username
# 加载某个资产的特殊配置认证信息
try:
self.load_asset_special_auth(asset, username)
except Exception as e:
logger.error('Load special auth Error: ', e)
pass
self.load_tmp_auth_if_has(asset_id, user)
@property
def cmd_filter_rules(self):
from .cmd_filter import CommandFilterRule

View File

@@ -47,22 +47,24 @@ class AssetUserReadSerializer(AssetUserWriteSerializer):
ip = serializers.CharField(read_only=True, label=_("IP"))
asset = serializers.CharField(source='asset_id', label=_('Asset'))
backend = serializers.CharField(read_only=True, label=_("Backend"))
backend_display = serializers.CharField(read_only=True, label=_("Source"))
class Meta(AssetUserWriteSerializer.Meta):
read_only_fields = (
'date_created', 'date_updated',
'created_by', 'version',
)
fields_mini = ['id', 'username']
fields_mini = ['id', 'name', 'username']
fields_write_only = ['password', 'private_key', "public_key"]
fields_small = fields_mini + fields_write_only + [
'backend', 'version',
'backend', 'backend_display', 'version',
'date_created', "date_updated",
'comment'
]
fields_fk = ['asset', 'hostname', 'ip']
fields = fields_small + fields_fk
extra_kwargs = {
'name': {'required': False},
'username': {'required': True},
'password': {'write_only': True},
'private_key': {'write_only': True},

View File

@@ -59,7 +59,7 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
'created_by', 'comment',
]
fields_fk = ['domain']
fields = fields_small + fields_fk
fields = fields_small + fields_fk
extra_kwargs = {
'password': {'write_only': True, 'validators': [NoSpecialChars()]},
'private_key': {"write_only": True},
@@ -78,12 +78,12 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
class GatewayWithAuthSerializer(GatewaySerializer):
def get_field_names(self, declared_fields, info):
fields = super().get_field_names(declared_fields, info)
fields.extend(
['password', 'private_key']
)
return fields
class Meta(GatewaySerializer.Meta):
extra_kwargs = {
'password': {'write_only': False, 'validators': [NoSpecialChars()]},
'private_key': {"write_only": False},
'public_key': {"write_only": False},
}
class DomainWithGatewaySerializer(BulkOrgResourceModelSerializer):

View File

@@ -14,6 +14,7 @@ __all__ = [
'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer',
'SystemUserNodeRelationSerializer', 'SystemUserTaskSerializer',
'SystemUserUserRelationSerializer', 'SystemUserWithAuthInfoSerializer',
'SystemUserTempAuthSerializer',
]
@@ -272,3 +273,10 @@ class SystemUserTaskSerializer(serializers.Serializer):
many=True
)
task = serializers.CharField(read_only=True)
class SystemUserTempAuthSerializer(SystemUserSerializer):
instance_id = serializers.CharField()
class Meta(SystemUserSerializer.Meta):
fields = ['instance_id', 'username', 'password']

View File

@@ -46,7 +46,9 @@ urlpatterns = [
path('system-users/<uuid:pk>/auth-info/', api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'),
path('system-users/<uuid:pk>/assets/', api.SystemUserAssetsListView.as_view(), name='system-user-assets'),
path('system-users/<uuid:pk>/assets/<uuid:aid>/auth-info/', api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'),
path('system-users/<uuid:pk>/assets/<uuid:asset_id>/auth-info/', api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'),
path('system-users/<uuid:pk>/applications/<uuid:app_id>/auth-info/', api.SystemUserAppAuthInfoApi.as_view(), name='system-user-app-auth-info'),
path('system-users/<uuid:pk>/temp-auth/', api.SystemUserTempAuthInfoApi.as_view(), name='system-user-asset-temp-info'),
path('system-users/<uuid:pk>/tasks/', api.SystemUserTaskApi.as_view(), name='system-user-task-create'),
path('system-users/<uuid:pk>/cmd-filter-rules/', api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'),

View File

@@ -94,8 +94,14 @@ class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet):
date_range_filter_fields = [
('date_start', ('date_from', 'date_to'))
]
filterset_fields = ['user__name', 'command', 'run_as__name', 'is_finished']
search_fields = ['command', 'user__name', 'run_as__name']
filterset_fields = [
'user__name', 'user__username', 'command',
'run_as__name', 'run_as__username', 'is_finished'
]
search_fields = [
'command', 'user__name', 'user__username',
'run_as__name', 'run_as__username',
]
ordering = ['-date_created']
def get_queryset(self):

View File

@@ -82,7 +82,7 @@ class CommandExecutionSerializer(serializers.ModelSerializer):
model = CommandExecution
fields_mini = ['id']
fields_small = fields_mini + [
'run_as', 'command', 'user', 'is_finished',
'run_as', 'command', 'is_finished', 'user',
'date_start', 'result', 'is_success', 'org_id'
]
fields = fields_small + ['hosts', 'hosts_display', 'run_as_display', 'user_display']

View File

@@ -57,6 +57,7 @@ class AuthBackendLabelMapping(LazyObject):
backend_label_mapping[settings.AUTH_BACKEND_PUBKEY] = _('SSH Key')
backend_label_mapping[settings.AUTH_BACKEND_MODEL] = _('Password')
backend_label_mapping[settings.AUTH_BACKEND_SSO] = _('SSO')
backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _('Auth Token')
backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _('WeCom')
backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _('DingTalk')
return backend_label_mapping
@@ -156,12 +157,13 @@ def get_login_backend(request):
return backend_label
def generate_data(username, request):
def generate_data(username, request, login_type=None):
user_agent = request.META.get('HTTP_USER_AGENT', '')
login_ip = get_request_ip(request) or '0.0.0.0'
if isinstance(request, Request):
if login_type is None and isinstance(request, Request):
login_type = request.META.get('HTTP_X_JMS_LOGIN_TYPE', 'U')
else:
if login_type is None:
login_type = 'W'
data = {
@@ -176,9 +178,9 @@ def generate_data(username, request):
@receiver(post_auth_success)
def on_user_auth_success(sender, user, request, **kwargs):
def on_user_auth_success(sender, user, request, login_type=None, **kwargs):
logger.debug('User login success: {}'.format(user.username))
data = generate_data(user.username, request)
data = generate_data(user.username, request, login_type=login_type)
data.update({'mfa': int(user.mfa_enabled), 'status': True})
write_login_log(**data)

View File

@@ -6,11 +6,14 @@ from django.conf import settings
from django.core.cache import cache
from django.shortcuts import get_object_or_404
from django.http import HttpResponse
from django.utils.translation import ugettext as _
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework import serializers
from authentication.signals import post_auth_failed, post_auth_success
from common.utils import get_logger, random_string
from common.drf.api import SerializerMixin2
from common.permissions import IsSuperUserOrAppUser, IsValidUser, IsSuperUser
@@ -49,11 +52,7 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
raise PermissionDenied(error)
return True
def create_token(self, user, asset, application, system_user):
if not settings.CONNECTION_TOKEN_ENABLED:
raise PermissionDenied('Connection token disabled')
if not user:
user = self.request.user
def create_token(self, user, asset, application, system_user, ttl=5*60):
if not self.request.user.is_superuser and user != self.request.user:
raise PermissionDenied('Only super user can create user token')
self.check_resource_permission(user, asset, application, system_user)
@@ -79,7 +78,7 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
})
key = self.CACHE_KEY_PREFIX.format(token)
cache.set(key, value, timeout=30*60)
cache.set(key, value, timeout=ttl)
return token
def create(self, request, *args, **kwargs):
@@ -93,14 +92,14 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
token = self.create_token(user, asset, application, system_user)
return Response({"token": token}, status=201)
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file')
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file', permission_classes=[IsValidUser])
def get_rdp_file(self, request, *args, **kwargs):
options = {
'full address:s': '',
'username:s': '',
'screen mode id:i': '0',
'desktopwidth:i': '1280',
'desktopheight:i': '800',
# 'desktopwidth:i': '1280',
# 'desktopheight:i': '800',
'use multimon:i': '1',
'session bpp:i': '32',
'audiomode:i': '0',
@@ -120,6 +119,8 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
'autoreconnection enabled:i': '1',
'bookmarktype:i': '3',
'use redirection server name:i': '0',
'smart sizing:i': '0',
# 'domain:s': ''
# 'alternate shell:s:': '||MySQLWorkbench',
# 'remoteapplicationname:s': 'Firefox',
# 'remoteapplicationcmdline:s': '',
@@ -134,17 +135,23 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
asset = serializer.validated_data.get('asset')
application = serializer.validated_data.get('application')
system_user = serializer.validated_data['system_user']
user = serializer.validated_data.get('user')
height = serializer.validated_data.get('height')
width = serializer.validated_data.get('width')
user = request.user
token = self.create_token(user, asset, application, system_user)
# Todo: 上线后地址是 JumpServerAddr:3389
address = self.request.query_params.get('address') or '1.1.1.1'
address = settings.TERMINAL_RDP_ADDR
if not address or address == 'localhost:3389':
address = request.get_host().split(':')[0] + ':3389'
options['full address:s'] = address
options['username:s'] = '{}|{}'.format(user.username, token)
options['desktopwidth:i'] = width
options['desktopheight:i'] = height
if system_user.ad_domain:
options['domain:s'] = system_user.ad_domain
if width and height:
options['desktopwidth:i'] = width
options['desktopheight:i'] = height
else:
options['smart sizing:i'] = '1'
data = ''
for k, v in options.items():
data += f'{k}:{v}\n'
@@ -155,10 +162,8 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
return response
@staticmethod
def _get_application_secret_detail(value):
from applications.models import Application
def _get_application_secret_detail(application):
from perms.models import Action
application = get_object_or_404(Application, id=value.get('application'))
gateway = None
if not application.category_remote_app:
@@ -184,15 +189,15 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
}
@staticmethod
def _get_asset_secret_detail(value, user, system_user):
from assets.models import Asset
def _get_asset_secret_detail(asset, user, system_user):
from perms.utils.asset import get_asset_system_user_ids_with_actions_by_user
asset = get_object_or_404(Asset, id=value.get('asset'))
systemuserid_actions_mapper = get_asset_system_user_ids_with_actions_by_user(user, asset)
actions = systemuserid_actions_mapper.get(system_user.id, [])
gateway = None
if asset and asset.domain and asset.domain.has_gateway():
gateway = asset.domain.random_gateway()
return {
'asset': asset,
'application': None,
@@ -201,29 +206,65 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
'actions': actions,
}
@action(methods=['POST'], detail=False, permission_classes=[IsSuperUserOrAppUser], url_path='secret-info/detail')
def get_secret_detail(self, request, *args, **kwargs):
def valid_token(self, token):
from users.models import User
from assets.models import SystemUser
from assets.models import SystemUser, Asset
from applications.models import Application
token = request.data.get('token', '')
key = self.CACHE_KEY_PREFIX.format(token)
value = cache.get(key, None)
if not value:
return Response(status=404)
user = get_object_or_404(User, id=value.get('user'))
system_user = get_object_or_404(SystemUser, id=value.get('system_user'))
data = dict(user=user, system_user=system_user)
raise serializers.ValidationError('Token not found')
user = get_object_or_404(User, id=value.get('user'))
if not user.is_valid:
raise serializers.ValidationError("User not valid, disabled or expired")
system_user = get_object_or_404(SystemUser, id=value.get('system_user'))
asset = None
app = None
if value.get('type') == 'asset':
asset_detail = self._get_asset_secret_detail(value, user=user, system_user=system_user)
asset = get_object_or_404(Asset, id=value.get('asset'))
else:
app = get_object_or_404(Application, id=value.get('application'))
if asset and not asset.is_active:
raise serializers.ValidationError("Asset disabled")
try:
self.check_resource_permission(user, asset, app, system_user)
except PermissionDenied:
raise serializers.ValidationError('Permission expired or invalid')
return value, user, system_user, asset, app
@action(methods=['POST'], detail=False, permission_classes=[IsSuperUserOrAppUser], url_path='secret-info/detail')
def get_secret_detail(self, request, *args, **kwargs):
token = request.data.get('token', '')
try:
value, user, system_user, asset, app = self.valid_token(token)
except serializers.ValidationError as e:
post_auth_failed.send(
sender=self.__class__, username='', request=self.request,
reason=_('Invalid token')
)
raise e
data = dict(user=user, system_user=system_user)
if asset:
asset_detail = self._get_asset_secret_detail(asset, user=user, system_user=system_user)
system_user.load_asset_more_auth(asset.id, user.username, user.id)
data['type'] = 'asset'
data.update(asset_detail)
else:
app_detail = self._get_application_secret_detail(value)
app_detail = self._get_application_secret_detail(app)
system_user.load_app_more_auth(app.id, user.id)
data['type'] = 'application'
data.update(app_detail)
self.request.session['auth_backend'] = settings.AUTH_BACKEND_AUTH_TOKEN
post_auth_success.send(sender=self.__class__, user=user, request=self.request, login_type='T')
serializer = self.get_serializer(data)
return Response(data=serializer.data, status=200)

View File

@@ -228,3 +228,11 @@ class DingTalkAuthentication(ModelBackend):
def authenticate(self, request, **kwargs):
pass
class AuthorizationTokenAuthentication(ModelBackend):
"""
什么也不做呀😺
"""
def authenticate(self, request, **kwargs):
pass

View File

@@ -199,5 +199,5 @@ class ConnectionTokenSecretSerializer(serializers.Serializer):
class RDPFileSerializer(ConnectionTokenSerializer):
width = serializers.IntegerField(default=1280)
height = serializers.IntegerField(default=800)
width = serializers.IntegerField(allow_null=True, max_value=3112, min_value=100, required=False)
height = serializers.IntegerField(allow_null=True, max_value=4096, min_value=100, required=False)

View File

@@ -18,7 +18,7 @@ from rest_framework.request import clone_request
class SimpleMetadataWithFilters(SimpleMetadata):
"""Override SimpleMetadata, adding info about filters"""
methods = {"PUT", "POST", "GET"}
methods = {"PUT", "POST", "GET", "PATCH"}
attrs = [
'read_only', 'label', 'help_text',
'min_length', 'max_length',
@@ -32,6 +32,9 @@ class SimpleMetadataWithFilters(SimpleMetadata):
"""
actions = {}
for method in self.methods & set(view.allowed_methods):
if hasattr(view, 'action_map'):
view.action = view.action_map.get(method.lower(), view.action)
view.request = clone_request(request, method)
try:
# Test global permissions

View File

@@ -47,8 +47,13 @@ class BaseFileParser(BaseParser):
def convert_to_field_names(self, column_titles):
fields_map = {}
fields = self.serializer_fields
fields_map.update({v.label: k for k, v in fields.items()})
fields_map.update({k: k for k, _ in fields.items()})
for k, v in fields.items():
if v.read_only:
continue
fields_map.update({
v.label: k,
k: k
})
field_names = [
fields_map.get(column_title.strip('*'), '')
for column_title in column_titles
@@ -94,7 +99,7 @@ class BaseFileParser(BaseParser):
new_row_data = {}
serializer_fields = self.serializer_fields
for k, v in row_data.items():
if isinstance(v, list) or isinstance(v, dict) or isinstance(v, str) and k.strip() and v.strip():
if type(v) in [list, dict, int] or (isinstance(v, str) and k.strip() and v.strip()):
# 解决类似disk_info为字符串的'{}'的问题
if not isinstance(v, str) and isinstance(serializer_fields[k], serializers.CharField):
v = str(v)

View File

@@ -259,6 +259,7 @@ class Config(dict):
'SECURITY_INSECURE_COMMAND': False,
'SECURITY_INSECURE_COMMAND_LEVEL': 5,
'SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER': '',
'SECURITY_LUNA_REMEMBER_AUTH': True,
'HTTP_BIND_HOST': '0.0.0.0',
'HTTP_LISTEN_PORT': 8080,
@@ -279,7 +280,7 @@ class Config(dict):
'WINDOWS_SSH_DEFAULT_SHELL': 'cmd',
'FLOWER_URL': "127.0.0.1:5555",
'DEFAULT_ORG_SHOW_ALL_USERS': True,
'PERIOD_TASK_ENABLE': True,
'PERIOD_TASK_ENABLED': True,
'FORCE_SCRIPT_NAME': '',
'LOGIN_CONFIRM_ENABLE': False,
'WINDOWS_SKIP_ALL_MANUAL_PASSWORD': False,
@@ -300,7 +301,9 @@ class Config(dict):
'SESSION_SAVE_EVERY_REQUEST': True,
'SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE': False,
'FORGOT_PASSWORD_URL': '',
'HEALTH_CHECK_TOKEN': ''
'HEALTH_CHECK_TOKEN': '',
'TERMINAL_RDP_ADDR': ''
}
def compatible_auth_openid_of_key(self):

View File

View File

@@ -0,0 +1,18 @@
from redis_sessions.session import force_unicode, SessionStore as RedisSessionStore
from redis import exceptions
class SessionStore(RedisSessionStore):
def load(self):
try:
session_data = self.server.get(
self.get_real_stored_key(self._get_or_create_session_key())
)
return self.decode(force_unicode(session_data))
except exceptions.ConnectionError as e:
# 解决redis服务异常(如: 主从切换时)用户session立即过期的问题
raise
except:
self._session_key = None
return {}

View File

@@ -2,9 +2,11 @@ from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from ops.urls.ws_urls import urlpatterns as ops_urlpatterns
from notifications.urls.ws_urls import urlpatterns as notifications_urlpatterns
urlpatterns = []
urlpatterns += ops_urlpatterns
urlpatterns += ops_urlpatterns \
+ notifications_urlpatterns
application = ProtocolTypeRouter({
'websocket': AuthMiddlewareStack(

View File

@@ -130,10 +130,12 @@ AUTH_BACKEND_CAS = 'authentication.backends.cas.CASBackend'
AUTH_BACKEND_SSO = 'authentication.backends.api.SSOAuthentication'
AUTH_BACKEND_WECOM = 'authentication.backends.api.WeComAuthentication'
AUTH_BACKEND_DINGTALK = 'authentication.backends.api.DingTalkAuthentication'
AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.api.AuthorizationTokenAuthentication'
AUTHENTICATION_BACKENDS = [
AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY, AUTH_BACKEND_WECOM, AUTH_BACKEND_DINGTALK
AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY, AUTH_BACKEND_WECOM,
AUTH_BACKEND_DINGTALK, AUTH_BACKEND_AUTH_TOKEN
]
if AUTH_CAS:

View File

@@ -48,6 +48,7 @@ INSTALLED_APPS = [
'applications.apps.ApplicationsConfig',
'tickets.apps.TicketsConfig',
'acls.apps.AclsConfig',
'notifications.apps.NotificationsConfig',
'common.apps.CommonConfig',
'jms_oidc_rp',
'rest_framework',
@@ -125,7 +126,7 @@ SESSION_EXPIRE_AT_BROWSER_CLOSE = True
# 自定义的配置SESSION_EXPIRE_AT_BROWSER_CLOSE 始终为 True, 下面这个来控制是否强制关闭后过期 cookie
SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE = CONFIG.SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE
SESSION_SAVE_EVERY_REQUEST = CONFIG.SESSION_SAVE_EVERY_REQUEST
SESSION_ENGINE = 'redis_sessions.session'
SESSION_ENGINE = 'jumpserver.rewriting.session'
SESSION_REDIS = {
'host': CONFIG.REDIS_HOST,
'port': CONFIG.REDIS_PORT,

View File

@@ -125,3 +125,6 @@ FORGOT_PASSWORD_URL = CONFIG.FORGOT_PASSWORD_URL
# 自定义默认组织名
GLOBAL_ORG_DISPLAY_NAME = CONFIG.GLOBAL_ORG_DISPLAY_NAME
HEALTH_CHECK_TOKEN = CONFIG.HEALTH_CHECK_TOKEN
TERMINAL_RDP_ADDR = CONFIG.TERMINAL_RDP_ADDR
SECURITY_LUNA_REMEMBER_AUTH = CONFIG.SECURITY_LUNA_REMEMBER_AUTH

View File

@@ -23,6 +23,7 @@ api_v1 = [
path('applications/', include('applications.urls.api_urls', namespace='api-applications')),
path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')),
path('acls/', include('acls.urls.api_urls', namespace='api-acls')),
path('notifications/', include('notifications.urls.api_urls', namespace='api-notifications')),
path('prometheus/metrics/', api.PrometheusMetricsApi.as_view()),
]

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

View File

@@ -0,0 +1,2 @@
from .notifications import *
from .site_msgs import *

View File

@@ -0,0 +1,72 @@
from django.http import Http404
from rest_framework.mixins import ListModelMixin, UpdateModelMixin
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from common.drf.api import JmsGenericViewSet
from notifications.notifications import system_msgs
from notifications.models import SystemMsgSubscription
from notifications.backends import BACKEND
from notifications.serializers import (
SystemMsgSubscriptionSerializer, SystemMsgSubscriptionByCategorySerializer
)
__all__ = ('BackendListView', 'SystemMsgSubscriptionViewSet')
class BackendListView(APIView):
def get(self, request):
data = [
{
'name': backend,
'name_display': backend.label
}
for backend in BACKEND
if backend.is_enable
]
return Response(data=data)
class SystemMsgSubscriptionViewSet(ListModelMixin,
UpdateModelMixin,
JmsGenericViewSet):
lookup_field = 'message_type'
queryset = SystemMsgSubscription.objects.all()
serializer_classes = {
'list': SystemMsgSubscriptionByCategorySerializer,
'update': SystemMsgSubscriptionSerializer,
'partial_update': SystemMsgSubscriptionSerializer
}
def list(self, request, *args, **kwargs):
data = []
category_children_mapper = {}
subscriptions = self.get_queryset()
msgtype_sub_mapper = {}
for sub in subscriptions:
msgtype_sub_mapper[sub.message_type] = sub
for msg in system_msgs:
message_type = msg['message_type']
message_type_label = msg['message_type_label']
category = msg['category']
category_label = msg['category_label']
if category not in category_children_mapper:
children = []
data.append({
'category': category,
'category_label': category_label,
'children': children
})
category_children_mapper[category] = children
sub = msgtype_sub_mapper[message_type]
sub.message_type_label = message_type_label
category_children_mapper[category].append(sub)
serializer = self.get_serializer(data, many=True)
return Response(data=serializer.data)

View File

@@ -0,0 +1,58 @@
from rest_framework.response import Response
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
from rest_framework.decorators import action
from common.http import is_true
from common.permissions import IsValidUser
from common.const.http import GET, PATCH, POST
from common.drf.api import JmsGenericViewSet
from ..serializers import (
SiteMessageDetailSerializer, SiteMessageIdsSerializer,
SiteMessageSendSerializer,
)
from ..site_msg import SiteMessageUtil
from ..filters import SiteMsgFilter
__all__ = ('SiteMessageViewSet', )
class SiteMessageViewSet(ListModelMixin, RetrieveModelMixin, JmsGenericViewSet):
permission_classes = (IsValidUser,)
serializer_classes = {
'default': SiteMessageDetailSerializer,
'mark_as_read': SiteMessageIdsSerializer,
'send': SiteMessageSendSerializer,
}
filterset_class = SiteMsgFilter
def get_queryset(self):
user = self.request.user
has_read = self.request.query_params.get('has_read')
if has_read is None:
msgs = SiteMessageUtil.get_user_all_msgs(user.id)
else:
msgs = SiteMessageUtil.filter_user_msgs(user.id, has_read=is_true(has_read))
return msgs
@action(methods=[GET], detail=False, url_path='unread-total')
def unread_total(self, request, **kwargs):
user = request.user
msgs = SiteMessageUtil.filter_user_msgs(user.id, has_read=False)
return Response(data={'total': msgs.count()})
@action(methods=[PATCH], detail=False, url_path='mark-as-read')
def mark_as_read(self, request, **kwargs):
user = request.user
seri = self.get_serializer(data=request.data)
seri.is_valid(raise_exception=True)
ids = seri.validated_data['ids']
SiteMessageUtil.mark_msgs_as_read(user.id, ids)
return Response({'detail': 'ok'})
@action(methods=[POST], detail=False)
def send(self, request, **kwargs):
seri = self.get_serializer(data=request.data)
seri.is_valid(raise_exception=True)
SiteMessageUtil.send_msg(**seri.validated_data, sender=request.user)
return Response({'detail': 'ok'})

View File

@@ -0,0 +1,9 @@
from django.apps import AppConfig
class NotificationsConfig(AppConfig):
name = 'notifications'
def ready(self):
from . import signals_handler
super().ready()

View File

@@ -0,0 +1,36 @@
from django.utils.translation import gettext_lazy as _
from django.db import models
from .dingtalk import DingTalk
from .email import Email
from .site_msg import SiteMessage
from .wecom import WeCom
class BACKEND(models.TextChoices):
EMAIL = 'email', _('Email')
WECOM = 'wecom', _('WeCom')
DINGTALK = 'dingtalk', _('DingTalk')
SITE_MSG = 'site_msg', _('Site message')
@property
def client(self):
client = {
self.EMAIL: Email,
self.WECOM: WeCom,
self.DINGTALK: DingTalk,
self.SITE_MSG: SiteMessage
}[self]
return client
def get_account(self, user):
return self.client.get_account(user)
@property
def is_enable(self):
return self.client.is_enable()
@classmethod
def filter_enable_backends(cls, backends):
enable_backends = [b for b in backends if cls(b).is_enable]
return enable_backends

View File

@@ -0,0 +1,32 @@
from django.conf import settings
class BackendBase:
# User 表中的字段
account_field = None
# Django setting 中的字段名
is_enable_field_in_settings = None
def get_accounts(self, users):
accounts = []
unbound_users = []
account_user_mapper = {}
for user in users:
account = getattr(user, self.account_field, None)
if account:
account_user_mapper[account] = user
accounts.append(account)
else:
unbound_users.append(user)
return accounts, unbound_users, account_user_mapper
@classmethod
def get_account(cls, user):
return getattr(user, cls.account_field)
@classmethod
def is_enable(cls):
enable = getattr(settings, cls.is_enable_field_in_settings)
return bool(enable)

View File

@@ -0,0 +1,20 @@
from django.conf import settings
from common.message.backends.dingtalk import DingTalk as Client
from .base import BackendBase
class DingTalk(BackendBase):
account_field = 'dingtalk_id'
is_enable_field_in_settings = 'AUTH_DINGTALK'
def __init__(self):
self.dingtalk = Client(
appid=settings.DINGTALK_APPKEY,
appsecret=settings.DINGTALK_APPSECRET,
agentid=settings.DINGTALK_AGENTID
)
def send_msg(self, users, msg):
accounts, __, __ = self.get_accounts(users)
return self.dingtalk.send_text(accounts, msg)

View File

@@ -0,0 +1,14 @@
from django.conf import settings
from django.core.mail import send_mail
from .base import BackendBase
class Email(BackendBase):
account_field = 'email'
is_enable_field_in_settings = 'EMAIL_HOST_USER'
def send_msg(self, users, subject, message):
from_email = settings.EMAIL_FROM or settings.EMAIL_HOST_USER
accounts, __, __ = self.get_accounts(users)
send_mail(subject, message, from_email, accounts, html_message=message)

View File

@@ -0,0 +1,14 @@
from notifications.site_msg import SiteMessageUtil as Client
from .base import BackendBase
class SiteMessage(BackendBase):
account_field = 'id'
def send_msg(self, users, subject, message):
accounts, __, __ = self.get_accounts(users)
Client.send_msg(subject, message, user_ids=accounts)
@classmethod
def is_enable(cls):
return True

View File

@@ -0,0 +1,20 @@
from django.conf import settings
from common.message.backends.wecom import WeCom as Client
from .base import BackendBase
class WeCom(BackendBase):
account_field = 'wecom_id'
is_enable_field_in_settings = 'AUTH_WECOM'
def __init__(self):
self.wecom = Client(
corpid=settings.WECOM_CORPID,
corpsecret=settings.WECOM_SECRET,
agentid=settings.WECOM_AGENTID
)
def send_msg(self, users, msg):
accounts, __, __ = self.get_accounts(users)
return self.wecom.send_text(accounts, msg)

View File

@@ -0,0 +1,18 @@
import django_filters
from common.drf.filters import BaseFilterSet
from .models import SiteMessage
class SiteMsgFilter(BaseFilterSet):
# 不用 Django 的关联表过滤有个小bug会重复关联相同表
# SELECT DISTINCT * FROM `notifications_sitemessage`
# INNER JOIN `notifications_sitemessageusers` ON (`notifications_sitemessage`.`id` = `notifications_sitemessageusers`.`sitemessage_id`)
# INNER JOIN `notifications_sitemessageusers` T4 ON (`notifications_sitemessage`.`id` = T4.`sitemessage_id`)
# WHERE (`notifications_sitemessageusers`.`user_id` = '40c8f140dfa246d4861b80f63cf4f6e3' AND NOT T4.`has_read`)
# ORDER BY `notifications_sitemessage`.`date_created` DESC LIMIT 15;
has_read = django_filters.BooleanFilter(method='do_nothing')
class Meta:
model = SiteMessage
fields = ('has_read',)

View File

@@ -0,0 +1,92 @@
# Generated by Django 3.1 on 2021-05-31 08:59
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('users', '0035_auto_20210526_1100'),
]
operations = [
migrations.CreateModel(
name='SiteMessage',
fields=[
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated 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')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('subject', models.CharField(max_length=1024)),
('message', models.TextField()),
('is_broadcast', models.BooleanField(default=False)),
('groups', models.ManyToManyField(to='users.UserGroup')),
('sender', models.ForeignKey(db_constraint=False, default=None, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='send_site_message', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='UserMsgSubscription',
fields=[
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated 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')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('message_type', models.CharField(max_length=128)),
('receive_backends', models.JSONField(default=list)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_msg_subscriptions', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='SystemMsgSubscription',
fields=[
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated 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')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('message_type', models.CharField(max_length=128, unique=True)),
('receive_backends', models.JSONField(default=list)),
('groups', models.ManyToManyField(related_name='system_msg_subscriptions', to='users.UserGroup')),
('users', models.ManyToManyField(related_name='system_msg_subscriptions', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='SiteMessageUsers',
fields=[
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated 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')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('has_read', models.BooleanField(default=False)),
('read_at', models.DateTimeField(default=None, null=True)),
('sitemessage', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, related_name='m2m_sitemessageusers', to='notifications.sitemessage')),
('user', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.CASCADE, related_name='m2m_sitemessageusers', to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='sitemessage',
name='users',
field=models.ManyToManyField(related_name='recv_site_messages', through='notifications.SiteMessageUsers', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,2 @@
from .notification import *
from .site_msg import *

View File

@@ -0,0 +1,50 @@
from django.db import models
from common.db.models import JMSModel
__all__ = ('SystemMsgSubscription', 'UserMsgSubscription')
class UserMsgSubscription(JMSModel):
message_type = models.CharField(max_length=128)
user = models.ForeignKey('users.User', related_name='user_msg_subscriptions', on_delete=models.CASCADE)
receive_backends = models.JSONField(default=list)
def __str__(self):
return f'{self.message_type}'
class SystemMsgSubscription(JMSModel):
message_type = models.CharField(max_length=128, unique=True)
users = models.ManyToManyField('users.User', related_name='system_msg_subscriptions')
groups = models.ManyToManyField('users.UserGroup', related_name='system_msg_subscriptions')
receive_backends = models.JSONField(default=list)
message_type_label = ''
def __str__(self):
return f'{self.message_type}'
def __repr__(self):
return self.__str__()
@property
def receivers(self):
from notifications.backends import BACKEND
users = [user for user in self.users.all()]
for group in self.groups.all():
for user in group.users.all():
users.append(user)
receive_backends = self.receive_backends
receviers = []
for user in users:
recevier = {'name': str(user), 'id': user.id}
for backend in receive_backends:
recevier[backend] = bool(BACKEND(backend).get_account(user))
receviers.append(recevier)
return receviers

View File

@@ -0,0 +1,30 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from common.db.models import JMSModel
__all__ = ('SiteMessageUsers', 'SiteMessage')
class SiteMessageUsers(JMSModel):
sitemessage = models.ForeignKey('notifications.SiteMessage', on_delete=models.CASCADE, db_constraint=False, related_name='m2m_sitemessageusers')
user = models.ForeignKey('users.User', on_delete=models.CASCADE, db_constraint=False, related_name='m2m_sitemessageusers')
has_read = models.BooleanField(default=False)
read_at = models.DateTimeField(default=None, null=True)
class SiteMessage(JMSModel):
subject = models.CharField(max_length=1024)
message = models.TextField()
users = models.ManyToManyField(
'users.User', through=SiteMessageUsers, related_name='recv_site_messages'
)
groups = models.ManyToManyField('users.UserGroup')
is_broadcast = models.BooleanField(default=False)
sender = models.ForeignKey(
'users.User', db_constraint=False, on_delete=models.DO_NOTHING, null=True, default=None,
related_name='send_site_message'
)
has_read = False
read_at = None

View File

@@ -0,0 +1,137 @@
from typing import Iterable
import traceback
from itertools import chain
from django.db.utils import ProgrammingError
from celery import shared_task
from notifications.backends import BACKEND
from .models import SystemMsgSubscription
__all__ = ('SystemMessage', 'UserMessage')
system_msgs = []
user_msgs = []
class MessageType(type):
def __new__(cls, name, bases, attrs: dict):
clz = type.__new__(cls, name, bases, attrs)
if 'message_type_label' in attrs \
and 'category' in attrs \
and 'category_label' in attrs:
message_type = clz.get_message_type()
msg = {
'message_type': message_type,
'message_type_label': attrs['message_type_label'],
'category': attrs['category'],
'category_label': attrs['category_label'],
}
if issubclass(clz, SystemMessage):
system_msgs.append(msg)
try:
if not SystemMsgSubscription.objects.filter(message_type=message_type).exists():
sub = SystemMsgSubscription.objects.create(message_type=message_type)
clz.post_insert_to_db(sub)
except ProgrammingError as e:
if e.args[0] == 1146:
# 表不存在
pass
else:
raise
elif issubclass(clz, UserMessage):
user_msgs.append(msg)
return clz
@shared_task
def publish_task(msg):
msg.publish()
class Message(metaclass=MessageType):
"""
这里封装了什么?
封装不同消息的模板,提供统一的发送消息的接口
- publish 该方法的实现与消息订阅的表结构有关
- send_msg
"""
message_type_label: str
category: str
category_label: str
@classmethod
def get_message_type(cls):
return cls.__name__
def publish_async(self):
return publish_task.delay(self)
def publish(self):
raise NotImplementedError
def send_msg(self, users: Iterable, backends: Iterable = BACKEND):
for backend in backends:
try:
backend = BACKEND(backend)
get_msg_method = getattr(self, f'get_{backend}_msg', self.get_common_msg)
msg = get_msg_method()
client = backend.client()
if isinstance(msg, dict):
client.send_msg(users, **msg)
else:
client.send_msg(users, msg)
except:
traceback.print_exc()
def get_common_msg(self) -> str:
raise NotImplementedError
def get_dingtalk_msg(self) -> str:
return self.get_common_msg()
def get_wecom_msg(self) -> str:
return self.get_common_msg()
def get_email_msg(self) -> dict:
msg = self.get_common_msg()
return {
'subject': msg,
'message': msg
}
def get_site_msg_msg(self) -> dict:
return self.get_email_msg()
class SystemMessage(Message):
def publish(self):
subscription = SystemMsgSubscription.objects.get(
message_type=self.get_message_type()
)
# 只发送当前有效后端
receive_backends = subscription.receive_backends
receive_backends = BACKEND.filter_enable_backends(receive_backends)
users = [
*subscription.users.all(),
*chain(*[g.users.all() for g in subscription.groups.all()])
]
self.send_msg(users, receive_backends)
@classmethod
def post_insert_to_db(cls, subscription: SystemMsgSubscription):
pass
class UserMessage(Message):
pass

View File

@@ -0,0 +1,2 @@
from .notifications import *
from .site_msgs import *

View File

@@ -0,0 +1,29 @@
from rest_framework import serializers
from common.drf.serializers import BulkModelSerializer
from notifications.models import SystemMsgSubscription
class SystemMsgSubscriptionSerializer(BulkModelSerializer):
receive_backends = serializers.ListField(child=serializers.CharField())
class Meta:
model = SystemMsgSubscription
fields = (
'message_type', 'message_type_label',
'users', 'groups', 'receive_backends', 'receivers'
)
read_only_fields = (
'message_type', 'message_type_label', 'receivers'
)
extra_kwargs = {
'users': {'allow_empty': True},
'groups': {'allow_empty': True},
'receive_backends': {'required': True}
}
class SystemMsgSubscriptionByCategorySerializer(serializers.Serializer):
category = serializers.CharField()
category_label = serializers.CharField()
children = SystemMsgSubscriptionSerializer(many=True)

View File

@@ -0,0 +1,36 @@
from rest_framework.serializers import ModelSerializer
from rest_framework import serializers
from ..models import SiteMessage
class SenderMixin(ModelSerializer):
sender = serializers.SerializerMethodField()
def get_sender(self, site_msg):
sender = site_msg.sender
if sender:
return str(sender)
else:
return ''
class SiteMessageDetailSerializer(SenderMixin, ModelSerializer):
class Meta:
model = SiteMessage
fields = [
'id', 'subject', 'message', 'has_read', 'read_at',
'date_created', 'date_updated', 'sender',
]
class SiteMessageIdsSerializer(serializers.Serializer):
ids = serializers.ListField(child=serializers.UUIDField())
class SiteMessageSendSerializer(serializers.Serializer):
subject = serializers.CharField()
message = serializers.CharField()
user_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
group_ids = serializers.ListField(child=serializers.UUIDField(), required=False)
is_broadcast = serializers.BooleanField(default=False)

View File

@@ -0,0 +1,43 @@
import json
from django.utils.functional import LazyObject
from django.db.models.signals import post_save
from django.dispatch import receiver
from common.utils.connection import RedisPubSub
from common.utils import get_logger
from common.decorator import on_transaction_commit
from .models import SiteMessage
logger = get_logger(__name__)
def new_site_msg_pub_sub():
return RedisPubSub('notifications.SiteMessageCome')
class NewSiteMsgSubPub(LazyObject):
def _setup(self):
self._wrapped = new_site_msg_pub_sub()
new_site_msg_chan = NewSiteMsgSubPub()
@receiver(post_save, sender=SiteMessage)
@on_transaction_commit
def on_site_message_create(sender, instance, created, **kwargs):
if not created:
return
logger.debug('New site msg created, publish it')
user_ids = instance.users.all().values_list('id', flat=True)
user_ids = [str(i) for i in user_ids]
data = {
'id': str(instance.id),
'subject': instance.subject,
'message': instance.message,
'users': user_ids
}
data = json.dumps(data)
new_site_msg_chan.publish(data)

View File

@@ -0,0 +1,86 @@
from django.db.models import F
from django.db import transaction
from common.utils.timezone import now
from users.models import User
from .models import SiteMessage as SiteMessageModel, SiteMessageUsers
class SiteMessageUtil:
@classmethod
def send_msg(cls, subject, message, user_ids=(), group_ids=(),
sender=None, is_broadcast=False):
if not any((user_ids, group_ids, is_broadcast)):
raise ValueError('No recipient is specified')
with transaction.atomic():
site_msg = SiteMessageModel.objects.create(
subject=subject, message=message,
is_broadcast=is_broadcast, sender=sender,
)
if is_broadcast:
user_ids = User.objects.all().values_list('id', flat=True)
else:
if group_ids:
site_msg.groups.add(*group_ids)
user_ids_from_group = User.groups.through.objects.filter(
usergroup_id__in=group_ids
).values_list('user_id', flat=True)
user_ids = [*user_ids, *user_ids_from_group]
site_msg.users.add(*user_ids)
@classmethod
def get_user_all_msgs(cls, user_id):
site_msgs = SiteMessageModel.objects.filter(
m2m_sitemessageusers__user_id=user_id
).distinct().annotate(
has_read=F('m2m_sitemessageusers__has_read'),
read_at=F('m2m_sitemessageusers__read_at')
).order_by('-date_created')
return site_msgs
@classmethod
def get_user_all_msgs_count(cls, user_id):
site_msgs_count = SiteMessageModel.objects.filter(
m2m_sitemessageusers__user_id=user_id
).distinct().count()
return site_msgs_count
@classmethod
def filter_user_msgs(cls, user_id, has_read=False):
site_msgs = SiteMessageModel.objects.filter(
m2m_sitemessageusers__user_id=user_id,
m2m_sitemessageusers__has_read=has_read
).distinct().annotate(
has_read=F('m2m_sitemessageusers__has_read'),
read_at=F('m2m_sitemessageusers__read_at')
).order_by('-date_created')
return site_msgs
@classmethod
def get_user_unread_msgs_count(cls, user_id):
site_msgs_count = SiteMessageModel.objects.filter(
m2m_sitemessageusers__user_id=user_id,
m2m_sitemessageusers__has_read=False
).distinct().count()
return site_msgs_count
@classmethod
def mark_msgs_as_read(cls, user_id, msg_ids):
site_msg_users = SiteMessageUsers.objects.filter(
user_id=user_id, sitemessage_id__in=msg_ids,
has_read=False
)
for site_msg_user in site_msg_users:
site_msg_user.has_read = True
site_msg_user.read_at = now()
SiteMessageUsers.objects.bulk_update(
site_msg_users, fields=('has_read', 'read_at'))

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

View File

@@ -0,0 +1,15 @@
from rest_framework_bulk.routes import BulkRouter
from django.urls import path
from notifications import api
app_name = 'notifications'
router = BulkRouter()
router.register('system-msg-subscription', api.SystemMsgSubscriptionViewSet, 'system-msg-subscription')
router.register('site-message', api.SiteMessageViewSet, 'site-message')
urlpatterns = [
path('backends/', api.BackendListView.as_view(), name='backends')
] + router.urls

View File

@@ -0,0 +1,9 @@
from django.urls import path
from .. import ws
app_name = 'notifications'
urlpatterns = [
path('ws/notifications/site-msg/', ws.SiteMsgWebsocket, name='site-msg-ws'),
]

70
apps/notifications/ws.py Normal file
View File

@@ -0,0 +1,70 @@
import threading
import json
from channels.generic.websocket import JsonWebsocketConsumer
from common.utils import get_logger
from .models import SiteMessage
from .site_msg import SiteMessageUtil
from .signals_handler import new_site_msg_chan
logger = get_logger(__name__)
class SiteMsgWebsocket(JsonWebsocketConsumer):
disconnected = False
refresh_every_seconds = 10
def connect(self):
user = self.scope["user"]
if user.is_authenticated:
self.accept()
thread = threading.Thread(target=self.unread_site_msg_count)
thread.start()
else:
self.close()
def receive(self, text_data=None, bytes_data=None, **kwargs):
data = json.loads(text_data)
refresh_every_seconds = data.get('refresh_every_seconds')
try:
refresh_every_seconds = int(refresh_every_seconds)
except Exception as e:
logger.error(e)
return
if refresh_every_seconds > 0:
self.refresh_every_seconds = refresh_every_seconds
def send_unread_msg_count(self):
user_id = self.scope["user"].id
unread_count = SiteMessageUtil.get_user_unread_msgs_count(user_id)
logger.debug('Send unread count to user: {} {}'.format(user_id, unread_count))
self.send_json({'type': 'unread_count', 'unread_count': unread_count})
def unread_site_msg_count(self):
user_id = str(self.scope["user"].id)
self.send_unread_msg_count()
while not self.disconnected:
subscribe = new_site_msg_chan.subscribe()
for message in subscribe.listen():
if message['type'] != 'message':
continue
try:
msg = json.loads(message['data'].decode())
logger.debug('New site msg recv, may be mine: {}'.format(msg))
if not msg:
continue
users = msg.get('users', [])
logger.debug('Message users: {}'.format(users))
if user_id in users:
self.send_unread_msg_count()
except json.JSONDecoder as e:
logger.debug('Decode json error: ', e)
def disconnect(self, close_code):
self.disconnected = True
self.close()

View File

@@ -13,4 +13,5 @@ class OpsConfig(AppConfig):
from orgs.utils import set_current_org
set_current_org(Organization.root())
from .celery import signal_handler
from . import notifications
super().ready()

View File

@@ -9,7 +9,7 @@ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
from django.db import models
from terminal.utils import send_command_execution_alert_mail
from terminal.notifications import CommandExecutionAlert
from common.utils import lazyproperty
from orgs.models import Organization
from orgs.mixins.models import OrgModelMixin
@@ -99,12 +99,12 @@ class CommandExecution(OrgModelMixin):
else:
msg = _("Command `{}` is forbidden ........").format(self.command)
print('\033[31m' + msg + '\033[0m')
send_command_execution_alert_mail({
CommandExecutionAlert({
'input': self.command,
'assets': self.hosts.all(),
'user': str(self.user),
'risk_level': 5,
})
}).publish_async()
self.result = {"error": msg}
self.org_id = self.run_as.org_id
self.is_finished = True

26
apps/ops/notifications.py Normal file
View File

@@ -0,0 +1,26 @@
from django.utils.translation import gettext_lazy as _
from notifications.notifications import SystemMessage
from notifications.models import SystemMsgSubscription
from users.models import User
__all__ = ('ServerPerformanceMessage',)
class ServerPerformanceMessage(SystemMessage):
category = 'Operations'
category_label = _('Operations')
message_type_label = _('Server performance')
def __init__(self, path, usage):
self.path = path
self.usage = usage
def get_common_msg(self):
msg = _("Disk used more than 80%: {} => {}").format(self.path, self.usage.percent)
return msg
@classmethod
def post_insert_to_db(cls, subscription: SystemMsgSubscription):
admins = User.objects.filter(role=User.ROLE.ADMIN)
subscription.users.add(*admins)

View File

@@ -20,7 +20,7 @@ from .celery.utils import (
disable_celery_periodic_task, delete_celery_periodic_task
)
from .models import Task, CommandExecution, CeleryTask
from .utils import send_server_performance_mail
from .notifications import ServerPerformanceMessage
logger = get_logger(__file__)
@@ -143,7 +143,7 @@ def check_server_performance_period():
if path.startswith(uncheck_path):
need_check = False
if need_check and usage.percent > 80:
send_server_performance_mail(path, usage, usages)
ServerPerformanceMessage(path=path, usage=usage).publish()
@shared_task(queue="ansible")

View File

@@ -69,16 +69,6 @@ def update_or_create_ansible_task(
return task, created
def send_server_performance_mail(path, usage, usages):
from users.models import User
subject = _("Disk used more than 80%: {} => {}").format(path, usage.percent)
message = subject
admins = User.objects.filter(role=User.ROLE.ADMIN)
recipient_list = [u.email for u in admins if u.email]
logger.info(subject)
send_mail_async(subject, message, recipient_list, html_message=message)
def get_task_log_path(base_path, task_id, level=2):
task_id = str(task_id)
try:

View File

@@ -48,7 +48,6 @@ class OrgViewSet(BulkModelViewSet):
queryset = Organization.objects.all()
serializer_class = OrgSerializer
permission_classes = (IsSuperUserOrAppUser,)
org = None
def get_serializer_class(self):
mapper = {
@@ -58,32 +57,36 @@ class OrgViewSet(BulkModelViewSet):
return mapper.get(self.action, super().get_serializer_class())
@tmp_to_root_org()
def get_data_from_model(self, model):
def get_data_from_model(self, org, model):
if model == User:
data = model.objects.filter(
orgs__id=self.org.id,
m2m_org_members__role__in=[ROLE.USER, ROLE.ADMIN, ROLE.AUDITOR]
orgs__id=org.id, m2m_org_members__role__in=[ROLE.USER, ROLE.ADMIN, ROLE.AUDITOR]
)
elif model == Node:
# 节点不能手动删除,所以排除检查
data = model.objects.filter(org_id=self.org.id).exclude(parent_key='', key__regex=r'^[0-9]+$')
# 节点不能手动删除,所以排除检查
data = model.objects.filter(org_id=org.id).exclude(parent_key='', key__regex=r'^[0-9]+$')
else:
data = model.objects.filter(org_id=self.org.id)
data = model.objects.filter(org_id=org.id)
return data
def destroy(self, request, *args, **kwargs):
self.org = self.get_object()
def allow_bulk_destroy(self, qs, filtered):
return False
def perform_destroy(self, instance):
if str(current_org) == str(instance):
msg = _('The current organization ({}) cannot be deleted'.format(current_org))
raise PermissionDenied(detail=msg)
for model in org_related_models:
data = self.get_data_from_model(model)
if data:
msg = _('Have {} exists, Please delete').format(model._meta.verbose_name)
return Response(data={'error': msg}, status=status.HTTP_403_FORBIDDEN)
else:
if str(current_org) == str(self.org):
msg = _('The current organization cannot be deleted')
return Response(data={'error': msg}, status=status.HTTP_403_FORBIDDEN)
self.org.delete()
return Response({'msg': True}, status=status.HTTP_200_OK)
data = self.get_data_from_model(instance, model)
if not data:
continue
msg = _(
'The organization have resource ({}) cannot be deleted'
).format(model._meta.verbose_name)
raise PermissionDenied(detail=msg)
super().perform_destroy(instance)
class OrgMemberRelationBulkViewSet(JMSBulkRelationModelViewSet):

View File

@@ -78,12 +78,12 @@ class OrgResourceStatisticsCache(OrgRelatedCache):
return self.org
def compute_users_amount(self):
if self.org.is_root():
users_amount = User.objects.exclude(role='App').count()
else:
users_amount = OrganizationMember.objects.values(
'user_id'
).filter(org_id=self.org.id).distinct().count()
users = User.objects.exclude(role='App')
if not self.org.is_root():
users = users.filter(m2m_org_members__org_id=self.org.id)
users_amount = users.values('id').distinct().count()
return users_amount
def compute_assets_amount(self):

View File

@@ -167,10 +167,3 @@ def on_org_user_changed(action, instance, reverse, pk_set, **kwargs):
leaved_users = set(pk_set) - set(org.members.filter(id__in=user_pk_set).values_list('id', flat=True))
_clear_users_from_org(org, leaved_users)
@receiver(post_save, sender=User)
def on_user_create_refresh_cache(sender, instance, created, **kwargs):
if created:
default_org = Organization.default()
default_org.members.add(instance)

View File

@@ -83,14 +83,17 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
return queryset
def to_internal_value(self, data):
# 系统用户是必填项
system_users = data.get('system_users', [])
system_users_display = data.pop('system_users_display', '')
for i in range(len(system_users_display)):
system_user = SystemUser.objects.filter(name=system_users_display[i]).first()
if system_user and system_user.id not in system_users:
system_users.append(system_user.id)
data['system_users'] = system_users
if 'system_users_display' in data:
# system_users_display 转化为 system_users
system_users = data.get('system_users', [])
system_users_display = data.pop('system_users_display')
for name in system_users_display:
system_user = SystemUser.objects.filter(name=name).first()
if system_user and system_user.id not in system_users:
system_users.append(system_user.id)
data['system_users'] = system_users
return super().to_internal_value(data)
def perform_display_create(self, instance, **kwargs):

View File

@@ -115,6 +115,7 @@ class PublicSettingApi(generics.RetrieveAPIView):
"OLD_PASSWORD_HISTORY_LIMIT_COUNT": settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT,
"SECURITY_COMMAND_EXECUTION": settings.SECURITY_COMMAND_EXECUTION,
"SECURITY_PASSWORD_EXPIRATION_TIME": settings.SECURITY_PASSWORD_EXPIRATION_TIME,
"SECURITY_LUNA_REMEMBER_AUTH": settings.SECURITY_LUNA_REMEMBER_AUTH,
"XPACK_LICENSE_IS_VALID": has_valid_xpack_license(),
"LOGIN_TITLE": self.get_login_title(),
"LOGO_URLS": self.get_logo_urls(),

View File

@@ -13,8 +13,9 @@ __all__ = [
class BasicSettingSerializer(serializers.Serializer):
SITE_URL = serializers.URLField(
required=True, label=_("Site url"),
help_text=_('eg: http://demo.jumpserver.org:8080')
help_text=_('eg: http://dev.jumpserver.org:8080')
)
USER_GUIDE_URL = serializers.URLField(
required=False, allow_blank=True, allow_null=True, label=_("User guide url"),
help_text=_('User first login update profile done redirect to it')
@@ -133,6 +134,12 @@ class TerminalSettingSerializer(serializers.Serializer):
help_text=_('Units: days, Session, record, command will be delete if more than duration, only in database')
)
TERMINAL_TELNET_REGEX = serializers.CharField(allow_blank=True, max_length=1024, required=False, label=_('Telnet login regex'))
TERMINAL_RDP_ADDR = serializers.CharField(
required=False, label=_("RDP address"),
max_length=1024,
allow_blank=True,
help_text=_('RDP visit address, eg: dev.jumpserver.org:3389')
)
class SecuritySettingSerializer(serializers.Serializer):

View File

@@ -26,6 +26,7 @@ from common.const import LDAP_AD_ACCOUNT_DISABLE
from common.utils import timeit, get_logger
from users.utils import construct_user_email
from users.models import User
from orgs.models import Organization
from authentication.backends.ldap import LDAPAuthorizationBackend, LDAPUser
logger = get_logger(__file__)
@@ -364,12 +365,17 @@ class LDAPImportUtil(object):
def perform_import(self, users):
logger.info('Start perform import ldap users, count: {}'.format(len(users)))
errors = []
instances = []
for user in users:
try:
self.update_or_create(user)
obj, created = self.update_or_create(user)
if created:
instances.append(obj)
except Exception as e:
errors.append({user['username']: str(e)})
logger.error(e)
# 默认添加用户到 Default 组织
Organization.default().members.add(*instances)
logger.info('End perform import ldap users')
return errors

View File

@@ -4,28 +4,24 @@ import time
from django.conf import settings
from django.utils import timezone
from django.shortcuts import HttpResponse
from rest_framework import viewsets
from rest_framework import generics
from rest_framework.fields import DateTimeField
from rest_framework.response import Response
from rest_framework.decorators import action
from django.template import loader
from common.http import is_true
from terminal.models import CommandStorage, Command
from terminal.models import CommandStorage
from terminal.filters import CommandFilter
from orgs.utils import current_org
from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor, IsAppUser
from common.const.http import GET
from common.drf.api import JMSBulkModelViewSet
from common.utils import get_logger
from terminal.utils import send_command_alert_mail
from terminal.serializers import InsecureCommandAlertSerializer
from terminal.exceptions import StorageInvalid
from ..backends import (
get_command_storage, get_multi_command_storage,
SessionCommandSerializer,
)
from ..notifications import CommandAlertMessage
logger = get_logger(__name__)
__all__ = ['CommandViewSet', 'CommandExportApi', 'InsecureCommandAlertAPI']
@@ -208,8 +204,6 @@ class InsecureCommandAlertAPI(generics.CreateAPIView):
serializer.is_valid(raise_exception=True)
commands = serializer.validated_data
for command in commands:
if command['risk_level'] >= settings.SECURITY_INSECURE_COMMAND_LEVEL and \
settings.SECURITY_INSECURE_COMMAND and \
settings.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER:
send_command_alert_mail(command)
if command['risk_level'] >= settings.SECURITY_INSECURE_COMMAND_LEVEL:
CommandAlertMessage(command).publish_async()
return Response()

View File

@@ -10,4 +10,5 @@ class TerminalConfig(AppConfig):
def ready(self):
from . import signals_handler
from . import notifications
return super().ready()

View File

@@ -16,6 +16,7 @@ class ReplayStorageTypeChoices(TextChoices):
swift = 'swift', 'Swift'
oss = 'oss', 'OSS'
azure = 'azure', 'Azure'
obs = 'obs', 'OBS'
class CommandStorageTypeChoices(TextChoices):

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.6 on 2021-06-04 03:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('terminal', '0035_auto_20210517_1448'),
]
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'), ('obs', 'OBS')], default='server', max_length=16, verbose_name='Type'),
),
]

Some files were not shown because too many files have changed in this diff Show More