mirror of https://github.com/jumpserver/jumpserver
commit
1e8d9ba2ec
|
@ -15,6 +15,7 @@ dump.rdb
|
||||||
.tox
|
.tox
|
||||||
.cache/
|
.cache/
|
||||||
.idea/
|
.idea/
|
||||||
|
.vscode/
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
config.py
|
config.py
|
||||||
config.yml
|
config.yml
|
||||||
|
|
|
@ -265,6 +265,7 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||||
- [极速安装](https://docs.jumpserver.org/zh/master/install/setup_by_fast/)
|
- [极速安装](https://docs.jumpserver.org/zh/master/install/setup_by_fast/)
|
||||||
- [完整文档](https://docs.jumpserver.org)
|
- [完整文档](https://docs.jumpserver.org)
|
||||||
- [演示视频](https://www.bilibili.com/video/BV1ZV41127GB)
|
- [演示视频](https://www.bilibili.com/video/BV1ZV41127GB)
|
||||||
|
- [手动安装](https://github.com/jumpserver/installer)
|
||||||
|
|
||||||
## 组件项目
|
## 组件项目
|
||||||
- [Lina](https://github.com/jumpserver/lina) JumpServer Web UI 项目
|
- [Lina](https://github.com/jumpserver/lina) JumpServer Web UI 项目
|
||||||
|
|
|
@ -2,18 +2,49 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
|
from rest_framework import generics
|
||||||
|
|
||||||
from ..hands import IsOrgAdminOrAppUser
|
from ..hands import IsOrgAdminOrAppUser, IsOrgAdmin
|
||||||
from .. import models, serializers
|
from .. import models, serializers
|
||||||
|
from ..models import Application
|
||||||
|
from assets.models import SystemUser
|
||||||
|
from assets.serializers import SystemUserListSerializer
|
||||||
|
from perms.models import ApplicationPermission
|
||||||
|
from ..const import ApplicationCategoryChoices
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['ApplicationViewSet']
|
__all__ = ['ApplicationViewSet', 'ApplicationUserListApi']
|
||||||
|
|
||||||
|
|
||||||
class ApplicationViewSet(OrgBulkModelViewSet):
|
class ApplicationViewSet(OrgBulkModelViewSet):
|
||||||
model = models.Application
|
model = Application
|
||||||
filterset_fields = ('name', 'type', 'category')
|
filterset_fields = ('name', 'type', 'category')
|
||||||
search_fields = filterset_fields
|
search_fields = filterset_fields
|
||||||
permission_classes = (IsOrgAdminOrAppUser,)
|
permission_classes = (IsOrgAdminOrAppUser,)
|
||||||
serializer_class = serializers.ApplicationSerializer
|
serializer_class = serializers.ApplicationSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationUserListApi(generics.ListAPIView):
|
||||||
|
permission_classes = (IsOrgAdmin, )
|
||||||
|
filterset_fields = ('name', 'username')
|
||||||
|
search_fields = filterset_fields
|
||||||
|
serializer_class = SystemUserListSerializer
|
||||||
|
|
||||||
|
def get_application(self):
|
||||||
|
application = None
|
||||||
|
app_id = self.request.query_params.get('application_id')
|
||||||
|
if app_id:
|
||||||
|
application = Application.objects.get(id=app_id)
|
||||||
|
return application
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
queryset = SystemUser.objects.none()
|
||||||
|
application = self.get_application()
|
||||||
|
if not application:
|
||||||
|
return queryset
|
||||||
|
system_user_ids = ApplicationPermission.objects.filter(applications=application)\
|
||||||
|
.values_list('system_users', flat=True)
|
||||||
|
if not system_user_ids:
|
||||||
|
return queryset
|
||||||
|
queryset = SystemUser.objects.filter(id__in=system_user_ids)
|
||||||
|
return queryset
|
||||||
|
|
|
@ -14,6 +14,7 @@ router.register(r'applications', api.ApplicationViewSet, 'application')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('remote-apps/<uuid:pk>/connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'),
|
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')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,14 +3,13 @@ from django.shortcuts import get_object_or_404
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
|
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsValidUser
|
||||||
from common.drf.filters import CustomFilter
|
|
||||||
from orgs.mixins.api import OrgBulkModelViewSet
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
from orgs.mixins import generics
|
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 ..models import SystemUser, Asset
|
||||||
from .. import serializers
|
from .. import serializers
|
||||||
from ..serializers import SystemUserWithAuthInfoSerializer
|
from ..serializers import SystemUserWithAuthInfoSerializer, SystemUserTempAuthSerializer
|
||||||
from ..tasks import (
|
from ..tasks import (
|
||||||
push_system_user_to_assets_manual, test_system_user_connectivity_manual,
|
push_system_user_to_assets_manual, test_system_user_connectivity_manual,
|
||||||
push_system_user_to_assets
|
push_system_user_to_assets
|
||||||
|
@ -21,6 +20,7 @@ logger = get_logger(__file__)
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi',
|
'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi',
|
||||||
'SystemUserCommandFilterRuleListApi', 'SystemUserTaskApi', 'SystemUserAssetsListView',
|
'SystemUserCommandFilterRuleListApi', 'SystemUserTaskApi', 'SystemUserAssetsListView',
|
||||||
|
'SystemUserTempAuthInfoApi', 'SystemUserAppAuthInfoApi',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -57,6 +57,25 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
|
||||||
return Response(status=204)
|
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):
|
class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView):
|
||||||
"""
|
"""
|
||||||
Get system user with asset auth info
|
Get system user with asset auth info
|
||||||
|
@ -65,21 +84,29 @@ class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView):
|
||||||
permission_classes = (IsOrgAdminOrAppUser,)
|
permission_classes = (IsOrgAdminOrAppUser,)
|
||||||
serializer_class = SystemUserWithAuthInfoSerializer
|
serializer_class = SystemUserWithAuthInfoSerializer
|
||||||
|
|
||||||
def get_exception_handler(self):
|
def get_object(self):
|
||||||
def handler(e, context):
|
instance = super().get_object()
|
||||||
return Response({"error": str(e)}, status=400)
|
asset_id = self.kwargs.get('asset_id')
|
||||||
return handler
|
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):
|
def get_object(self):
|
||||||
instance = super().get_object()
|
instance = super().get_object()
|
||||||
username = instance.username
|
app_id = self.kwargs.get('app_id')
|
||||||
if instance.username_same_with_user:
|
user_id = self.request.query_params.get("user_id")
|
||||||
username = self.request.query_params.get("username")
|
if user_id:
|
||||||
asset_id = self.kwargs.get('aid')
|
instance.load_app_more_auth(app_id, user_id)
|
||||||
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
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -31,11 +31,11 @@ class BaseBackend:
|
||||||
def qs_to_values(qs):
|
def qs_to_values(qs):
|
||||||
values = qs.values(
|
values = qs.values(
|
||||||
'hostname', 'ip', "asset_id",
|
'hostname', 'ip', "asset_id",
|
||||||
'username', 'password', 'private_key', 'public_key',
|
'name', 'username', 'password', 'private_key', 'public_key',
|
||||||
'score', 'version',
|
'score', 'version',
|
||||||
"asset_username", "union_id",
|
"asset_username", "union_id",
|
||||||
'date_created', 'date_updated',
|
'date_created', 'date_updated',
|
||||||
'org_id', 'backend',
|
'org_id', 'backend', 'backend_display'
|
||||||
)
|
)
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
|
@ -106,6 +106,7 @@ class DBBackend(BaseBackend):
|
||||||
class SystemUserBackend(DBBackend):
|
class SystemUserBackend(DBBackend):
|
||||||
model = SystemUser.assets.through
|
model = SystemUser.assets.through
|
||||||
backend = 'system_user'
|
backend = 'system_user'
|
||||||
|
backend_display = _('System user')
|
||||||
prefer = backend
|
prefer = backend
|
||||||
base_score = 0
|
base_score = 0
|
||||||
union_id_length = 2
|
union_id_length = 2
|
||||||
|
@ -138,6 +139,7 @@ class SystemUserBackend(DBBackend):
|
||||||
kwargs = dict(
|
kwargs = dict(
|
||||||
hostname=F("asset__hostname"),
|
hostname=F("asset__hostname"),
|
||||||
ip=F("asset__ip"),
|
ip=F("asset__ip"),
|
||||||
|
name=F("systemuser__name"),
|
||||||
username=F("systemuser__username"),
|
username=F("systemuser__username"),
|
||||||
password=F("systemuser__password"),
|
password=F("systemuser__password"),
|
||||||
private_key=F("systemuser__private_key"),
|
private_key=F("systemuser__private_key"),
|
||||||
|
@ -152,7 +154,8 @@ class SystemUserBackend(DBBackend):
|
||||||
union_id=Concat(F("systemuser_id"), Value("_"), F("asset_id"),
|
union_id=Concat(F("systemuser_id"), Value("_"), F("asset_id"),
|
||||||
output_field=CharField()),
|
output_field=CharField()),
|
||||||
org_id=F("asset__org_id"),
|
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
|
return kwargs
|
||||||
|
|
||||||
|
@ -174,12 +177,17 @@ class SystemUserBackend(DBBackend):
|
||||||
|
|
||||||
class DynamicSystemUserBackend(SystemUserBackend):
|
class DynamicSystemUserBackend(SystemUserBackend):
|
||||||
backend = 'system_user_dynamic'
|
backend = 'system_user_dynamic'
|
||||||
|
backend_display = _('System user(Dynamic)')
|
||||||
prefer = 'system_user'
|
prefer = 'system_user'
|
||||||
union_id_length = 3
|
union_id_length = 3
|
||||||
|
|
||||||
def get_annotate(self):
|
def get_annotate(self):
|
||||||
kwargs = super().get_annotate()
|
kwargs = super().get_annotate()
|
||||||
kwargs.update(dict(
|
kwargs.update(dict(
|
||||||
|
name=Concat(
|
||||||
|
F("systemuser__users__name"), Value('('), F("systemuser__name"), Value(')'),
|
||||||
|
output_field=CharField()
|
||||||
|
),
|
||||||
username=F("systemuser__users__username"),
|
username=F("systemuser__users__username"),
|
||||||
asset_username=Concat(
|
asset_username=Concat(
|
||||||
F("asset__id"), Value("_"),
|
F("asset__id"), Value("_"),
|
||||||
|
@ -221,6 +229,7 @@ class DynamicSystemUserBackend(SystemUserBackend):
|
||||||
class AdminUserBackend(DBBackend):
|
class AdminUserBackend(DBBackend):
|
||||||
model = Asset
|
model = Asset
|
||||||
backend = 'admin_user'
|
backend = 'admin_user'
|
||||||
|
backend_display = _('Admin user')
|
||||||
prefer = backend
|
prefer = backend
|
||||||
base_score = 200
|
base_score = 200
|
||||||
|
|
||||||
|
@ -246,6 +255,7 @@ class AdminUserBackend(DBBackend):
|
||||||
def all(self):
|
def all(self):
|
||||||
qs = self.model.objects.all().annotate(
|
qs = self.model.objects.all().annotate(
|
||||||
asset_id=F("id"),
|
asset_id=F("id"),
|
||||||
|
name=F("admin_user__name"),
|
||||||
username=F("admin_user__username"),
|
username=F("admin_user__username"),
|
||||||
password=F("admin_user__password"),
|
password=F("admin_user__password"),
|
||||||
private_key=F("admin_user__private_key"),
|
private_key=F("admin_user__private_key"),
|
||||||
|
@ -256,6 +266,7 @@ class AdminUserBackend(DBBackend):
|
||||||
asset_username=Concat(F("id"), Value("_"), F("admin_user__username"), output_field=CharField()),
|
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()),
|
union_id=Concat(F("admin_user_id"), Value("_"), F("id"), output_field=CharField()),
|
||||||
backend=Value(self.backend, CharField()),
|
backend=Value(self.backend, CharField()),
|
||||||
|
backend_display=Value(self.backend_display, CharField()),
|
||||||
)
|
)
|
||||||
qs = self.qs_to_values(qs)
|
qs = self.qs_to_values(qs)
|
||||||
return qs
|
return qs
|
||||||
|
@ -264,6 +275,7 @@ class AdminUserBackend(DBBackend):
|
||||||
class AuthbookBackend(DBBackend):
|
class AuthbookBackend(DBBackend):
|
||||||
model = AuthBook
|
model = AuthBook
|
||||||
backend = 'db'
|
backend = 'db'
|
||||||
|
backend_display = _('Database')
|
||||||
prefer = backend
|
prefer = backend
|
||||||
base_score = 400
|
base_score = 400
|
||||||
|
|
||||||
|
@ -313,6 +325,7 @@ class AuthbookBackend(DBBackend):
|
||||||
asset_username=Concat(F("asset__id"), Value("_"), F("username"), output_field=CharField()),
|
asset_username=Concat(F("asset__id"), Value("_"), F("username"), output_field=CharField()),
|
||||||
union_id=Concat(F("id"), Value("_"), F("asset_id"), output_field=CharField()),
|
union_id=Concat(F("id"), Value("_"), F("asset_id"), output_field=CharField()),
|
||||||
backend=Value(self.backend, CharField()),
|
backend=Value(self.backend, CharField()),
|
||||||
|
backend_display=Value(self.backend_display, CharField()),
|
||||||
)
|
)
|
||||||
qs = self.qs_to_values(qs)
|
qs = self.qs_to_values(qs)
|
||||||
return qs
|
return qs
|
||||||
|
|
|
@ -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'},
|
||||||
|
),
|
||||||
|
]
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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',
|
||||||
|
),
|
||||||
|
]
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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),
|
||||||
|
]
|
|
@ -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')},
|
||||||
|
),
|
||||||
|
]
|
|
@ -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')},
|
||||||
|
),
|
||||||
|
]
|
|
@ -7,6 +7,7 @@ class AssetUser(AuthBook):
|
||||||
hostname = ""
|
hostname = ""
|
||||||
ip = ""
|
ip = ""
|
||||||
backend = ""
|
backend = ""
|
||||||
|
backend_display = ""
|
||||||
union_id = ""
|
union_id = ""
|
||||||
asset_username = ""
|
asset_username = ""
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,7 @@ from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from common.db.models import ChoiceSet
|
from common.utils import random_string, signer
|
||||||
from common.utils import random_string
|
|
||||||
from common.utils import (
|
from common.utils import (
|
||||||
ssh_key_string_to_obj, ssh_key_gen, get_logger, lazyproperty
|
ssh_key_string_to_obj, ssh_key_gen, get_logger, lazyproperty
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,9 +7,10 @@ import logging
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||||
|
from django.core.cache import cache
|
||||||
|
|
||||||
from common.utils import signer
|
from common.utils import signer, get_object_or_none
|
||||||
from common.fields.model import JsonListCharField
|
from common.exceptions import JMSException
|
||||||
from .base import BaseUser
|
from .base import BaseUser
|
||||||
from .asset import Asset
|
from .asset import Asset
|
||||||
|
|
||||||
|
@ -185,6 +186,81 @@ class SystemUser(BaseUser):
|
||||||
if self.username_same_with_user:
|
if self.username_same_with_user:
|
||||||
self.username = other.username
|
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
|
@property
|
||||||
def cmd_filter_rules(self):
|
def cmd_filter_rules(self):
|
||||||
from .cmd_filter import CommandFilterRule
|
from .cmd_filter import CommandFilterRule
|
||||||
|
|
|
@ -47,22 +47,24 @@ class AssetUserReadSerializer(AssetUserWriteSerializer):
|
||||||
ip = serializers.CharField(read_only=True, label=_("IP"))
|
ip = serializers.CharField(read_only=True, label=_("IP"))
|
||||||
asset = serializers.CharField(source='asset_id', label=_('Asset'))
|
asset = serializers.CharField(source='asset_id', label=_('Asset'))
|
||||||
backend = serializers.CharField(read_only=True, label=_("Backend"))
|
backend = serializers.CharField(read_only=True, label=_("Backend"))
|
||||||
|
backend_display = serializers.CharField(read_only=True, label=_("Source"))
|
||||||
|
|
||||||
class Meta(AssetUserWriteSerializer.Meta):
|
class Meta(AssetUserWriteSerializer.Meta):
|
||||||
read_only_fields = (
|
read_only_fields = (
|
||||||
'date_created', 'date_updated',
|
'date_created', 'date_updated',
|
||||||
'created_by', 'version',
|
'created_by', 'version',
|
||||||
)
|
)
|
||||||
fields_mini = ['id', 'username']
|
fields_mini = ['id', 'name', 'username']
|
||||||
fields_write_only = ['password', 'private_key', "public_key"]
|
fields_write_only = ['password', 'private_key', "public_key"]
|
||||||
fields_small = fields_mini + fields_write_only + [
|
fields_small = fields_mini + fields_write_only + [
|
||||||
'backend', 'version',
|
'backend', 'backend_display', 'version',
|
||||||
'date_created', "date_updated",
|
'date_created', "date_updated",
|
||||||
'comment'
|
'comment'
|
||||||
]
|
]
|
||||||
fields_fk = ['asset', 'hostname', 'ip']
|
fields_fk = ['asset', 'hostname', 'ip']
|
||||||
fields = fields_small + fields_fk
|
fields = fields_small + fields_fk
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
|
'name': {'required': False},
|
||||||
'username': {'required': True},
|
'username': {'required': True},
|
||||||
'password': {'write_only': True},
|
'password': {'write_only': True},
|
||||||
'private_key': {'write_only': True},
|
'private_key': {'write_only': True},
|
||||||
|
|
|
@ -14,6 +14,7 @@ __all__ = [
|
||||||
'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer',
|
'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer',
|
||||||
'SystemUserNodeRelationSerializer', 'SystemUserTaskSerializer',
|
'SystemUserNodeRelationSerializer', 'SystemUserTaskSerializer',
|
||||||
'SystemUserUserRelationSerializer', 'SystemUserWithAuthInfoSerializer',
|
'SystemUserUserRelationSerializer', 'SystemUserWithAuthInfoSerializer',
|
||||||
|
'SystemUserTempAuthSerializer',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -272,3 +273,10 @@ class SystemUserTaskSerializer(serializers.Serializer):
|
||||||
many=True
|
many=True
|
||||||
)
|
)
|
||||||
task = serializers.CharField(read_only=True)
|
task = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class SystemUserTempAuthSerializer(SystemUserSerializer):
|
||||||
|
instance_id = serializers.CharField()
|
||||||
|
|
||||||
|
class Meta(SystemUserSerializer.Meta):
|
||||||
|
fields = ['instance_id', 'username', 'password']
|
||||||
|
|
|
@ -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>/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/', 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>/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'),
|
path('system-users/<uuid:pk>/cmd-filter-rules/', api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'),
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ from rest_framework.response import Response
|
||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
from common.utils import get_logger, random_string
|
from common.utils import get_logger, random_string
|
||||||
from common.drf.api import SerializerMixin2
|
from common.drf.api import SerializerMixin2
|
||||||
|
@ -49,7 +50,7 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
||||||
raise PermissionDenied(error)
|
raise PermissionDenied(error)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def create_token(self, user, asset, application, system_user):
|
def create_token(self, user, asset, application, system_user, ttl=5*60):
|
||||||
if not settings.CONNECTION_TOKEN_ENABLED:
|
if not settings.CONNECTION_TOKEN_ENABLED:
|
||||||
raise PermissionDenied('Connection token disabled')
|
raise PermissionDenied('Connection token disabled')
|
||||||
if not user:
|
if not user:
|
||||||
|
@ -79,7 +80,7 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
||||||
})
|
})
|
||||||
|
|
||||||
key = self.CACHE_KEY_PREFIX.format(token)
|
key = self.CACHE_KEY_PREFIX.format(token)
|
||||||
cache.set(key, value, timeout=30*60)
|
cache.set(key, value, timeout=ttl)
|
||||||
return token
|
return token
|
||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
|
@ -93,14 +94,14 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
||||||
token = self.create_token(user, asset, application, system_user)
|
token = self.create_token(user, asset, application, system_user)
|
||||||
return Response({"token": token}, status=201)
|
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):
|
def get_rdp_file(self, request, *args, **kwargs):
|
||||||
options = {
|
options = {
|
||||||
'full address:s': '',
|
'full address:s': '',
|
||||||
'username:s': '',
|
'username:s': '',
|
||||||
'screen mode id:i': '0',
|
'screen mode id:i': '0',
|
||||||
'desktopwidth:i': '1280',
|
# 'desktopwidth:i': '1280',
|
||||||
'desktopheight:i': '800',
|
# 'desktopheight:i': '800',
|
||||||
'use multimon:i': '1',
|
'use multimon:i': '1',
|
||||||
'session bpp:i': '32',
|
'session bpp:i': '32',
|
||||||
'audiomode:i': '0',
|
'audiomode:i': '0',
|
||||||
|
@ -120,6 +121,8 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
||||||
'autoreconnection enabled:i': '1',
|
'autoreconnection enabled:i': '1',
|
||||||
'bookmarktype:i': '3',
|
'bookmarktype:i': '3',
|
||||||
'use redirection server name:i': '0',
|
'use redirection server name:i': '0',
|
||||||
|
'smart sizing:i': '0',
|
||||||
|
# 'domain:s': ''
|
||||||
# 'alternate shell:s:': '||MySQLWorkbench',
|
# 'alternate shell:s:': '||MySQLWorkbench',
|
||||||
# 'remoteapplicationname:s': 'Firefox',
|
# 'remoteapplicationname:s': 'Firefox',
|
||||||
# 'remoteapplicationcmdline:s': '',
|
# 'remoteapplicationcmdline:s': '',
|
||||||
|
@ -134,17 +137,23 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
||||||
asset = serializer.validated_data.get('asset')
|
asset = serializer.validated_data.get('asset')
|
||||||
application = serializer.validated_data.get('application')
|
application = serializer.validated_data.get('application')
|
||||||
system_user = serializer.validated_data['system_user']
|
system_user = serializer.validated_data['system_user']
|
||||||
user = serializer.validated_data.get('user')
|
|
||||||
height = serializer.validated_data.get('height')
|
height = serializer.validated_data.get('height')
|
||||||
width = serializer.validated_data.get('width')
|
width = serializer.validated_data.get('width')
|
||||||
|
user = request.user
|
||||||
token = self.create_token(user, asset, application, system_user)
|
token = self.create_token(user, asset, application, system_user)
|
||||||
|
|
||||||
# Todo: 上线后地址是 JumpServerAddr:3389
|
address = settings.TERMINAL_RDP_ADDR
|
||||||
address = self.request.query_params.get('address') or '1.1.1.1'
|
if not address or address == 'localhost:3389':
|
||||||
|
address = request.get_host().split(':')[0] + ':3389'
|
||||||
options['full address:s'] = address
|
options['full address:s'] = address
|
||||||
options['username:s'] = '{}|{}'.format(user.username, token)
|
options['username:s'] = '{}|{}'.format(user.username, token)
|
||||||
|
if system_user.ad_domain:
|
||||||
|
options['domain:s'] = system_user.ad_domain
|
||||||
|
if width and height:
|
||||||
options['desktopwidth:i'] = width
|
options['desktopwidth:i'] = width
|
||||||
options['desktopheight:i'] = height
|
options['desktopheight:i'] = height
|
||||||
|
else:
|
||||||
|
options['smart sizing:i'] = '1'
|
||||||
data = ''
|
data = ''
|
||||||
for k, v in options.items():
|
for k, v in options.items():
|
||||||
data += f'{k}:{v}\n'
|
data += f'{k}:{v}\n'
|
||||||
|
@ -155,10 +164,8 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_application_secret_detail(value):
|
def _get_application_secret_detail(application):
|
||||||
from applications.models import Application
|
|
||||||
from perms.models import Action
|
from perms.models import Action
|
||||||
application = get_object_or_404(Application, id=value.get('application'))
|
|
||||||
gateway = None
|
gateway = None
|
||||||
|
|
||||||
if not application.category_remote_app:
|
if not application.category_remote_app:
|
||||||
|
@ -184,15 +191,15 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_asset_secret_detail(value, user, system_user):
|
def _get_asset_secret_detail(asset, user, system_user):
|
||||||
from assets.models import Asset
|
|
||||||
from perms.utils.asset import get_asset_system_user_ids_with_actions_by_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)
|
systemuserid_actions_mapper = get_asset_system_user_ids_with_actions_by_user(user, asset)
|
||||||
actions = systemuserid_actions_mapper.get(system_user.id, [])
|
actions = systemuserid_actions_mapper.get(system_user.id, [])
|
||||||
|
|
||||||
gateway = None
|
gateway = None
|
||||||
if asset and asset.domain and asset.domain.has_gateway():
|
if asset and asset.domain and asset.domain.has_gateway():
|
||||||
gateway = asset.domain.random_gateway()
|
gateway = asset.domain.random_gateway()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'asset': asset,
|
'asset': asset,
|
||||||
'application': None,
|
'application': None,
|
||||||
|
@ -201,26 +208,47 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
||||||
'actions': actions,
|
'actions': actions,
|
||||||
}
|
}
|
||||||
|
|
||||||
@action(methods=['POST'], detail=False, permission_classes=[IsSuperUserOrAppUser], url_path='secret-info/detail')
|
def valid_token(self, token):
|
||||||
def get_secret_detail(self, request, *args, **kwargs):
|
|
||||||
from users.models import User
|
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)
|
key = self.CACHE_KEY_PREFIX.format(token)
|
||||||
value = cache.get(key, None)
|
value = cache.get(key, None)
|
||||||
if not value:
|
if not value:
|
||||||
return Response(status=404)
|
raise serializers.ValidationError('Token not found')
|
||||||
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)
|
|
||||||
|
|
||||||
|
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':
|
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")
|
||||||
|
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', '')
|
||||||
|
value, user, system_user, asset, app = self.valid_token(token)
|
||||||
|
|
||||||
|
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['type'] = 'asset'
|
||||||
data.update(asset_detail)
|
data.update(asset_detail)
|
||||||
else:
|
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['type'] = 'application'
|
||||||
data.update(app_detail)
|
data.update(app_detail)
|
||||||
|
|
||||||
|
|
|
@ -199,5 +199,5 @@ class ConnectionTokenSecretSerializer(serializers.Serializer):
|
||||||
|
|
||||||
|
|
||||||
class RDPFileSerializer(ConnectionTokenSerializer):
|
class RDPFileSerializer(ConnectionTokenSerializer):
|
||||||
width = serializers.IntegerField(default=1280)
|
width = serializers.IntegerField(allow_null=True, max_value=3112, min_value=100, required=False)
|
||||||
height = serializers.IntegerField(default=800)
|
height = serializers.IntegerField(allow_null=True, max_value=4096, min_value=100, required=False)
|
||||||
|
|
|
@ -18,7 +18,7 @@ from rest_framework.request import clone_request
|
||||||
class SimpleMetadataWithFilters(SimpleMetadata):
|
class SimpleMetadataWithFilters(SimpleMetadata):
|
||||||
"""Override SimpleMetadata, adding info about filters"""
|
"""Override SimpleMetadata, adding info about filters"""
|
||||||
|
|
||||||
methods = {"PUT", "POST", "GET"}
|
methods = {"PUT", "POST", "GET", "PATCH"}
|
||||||
attrs = [
|
attrs = [
|
||||||
'read_only', 'label', 'help_text',
|
'read_only', 'label', 'help_text',
|
||||||
'min_length', 'max_length',
|
'min_length', 'max_length',
|
||||||
|
@ -32,6 +32,9 @@ class SimpleMetadataWithFilters(SimpleMetadata):
|
||||||
"""
|
"""
|
||||||
actions = {}
|
actions = {}
|
||||||
for method in self.methods & set(view.allowed_methods):
|
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)
|
view.request = clone_request(request, method)
|
||||||
try:
|
try:
|
||||||
# Test global permissions
|
# Test global permissions
|
||||||
|
|
|
@ -94,7 +94,7 @@ class BaseFileParser(BaseParser):
|
||||||
new_row_data = {}
|
new_row_data = {}
|
||||||
serializer_fields = self.serializer_fields
|
serializer_fields = self.serializer_fields
|
||||||
for k, v in row_data.items():
|
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为字符串的'{}'的问题
|
# 解决类似disk_info为字符串的'{}'的问题
|
||||||
if not isinstance(v, str) and isinstance(serializer_fields[k], serializers.CharField):
|
if not isinstance(v, str) and isinstance(serializer_fields[k], serializers.CharField):
|
||||||
v = str(v)
|
v = str(v)
|
||||||
|
|
|
@ -259,6 +259,7 @@ class Config(dict):
|
||||||
'SECURITY_INSECURE_COMMAND': False,
|
'SECURITY_INSECURE_COMMAND': False,
|
||||||
'SECURITY_INSECURE_COMMAND_LEVEL': 5,
|
'SECURITY_INSECURE_COMMAND_LEVEL': 5,
|
||||||
'SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER': '',
|
'SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER': '',
|
||||||
|
'SECURITY_LUNA_REMEMBER_AUTH': True,
|
||||||
|
|
||||||
'HTTP_BIND_HOST': '0.0.0.0',
|
'HTTP_BIND_HOST': '0.0.0.0',
|
||||||
'HTTP_LISTEN_PORT': 8080,
|
'HTTP_LISTEN_PORT': 8080,
|
||||||
|
@ -300,7 +301,9 @@ class Config(dict):
|
||||||
'SESSION_SAVE_EVERY_REQUEST': True,
|
'SESSION_SAVE_EVERY_REQUEST': True,
|
||||||
'SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE': False,
|
'SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE': False,
|
||||||
'FORGOT_PASSWORD_URL': '',
|
'FORGOT_PASSWORD_URL': '',
|
||||||
'HEALTH_CHECK_TOKEN': ''
|
'HEALTH_CHECK_TOKEN': '',
|
||||||
|
|
||||||
|
'TERMINAL_RDP_ADDR': ''
|
||||||
}
|
}
|
||||||
|
|
||||||
def compatible_auth_openid_of_key(self):
|
def compatible_auth_openid_of_key(self):
|
||||||
|
|
|
@ -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 {}
|
|
@ -48,6 +48,7 @@ INSTALLED_APPS = [
|
||||||
'applications.apps.ApplicationsConfig',
|
'applications.apps.ApplicationsConfig',
|
||||||
'tickets.apps.TicketsConfig',
|
'tickets.apps.TicketsConfig',
|
||||||
'acls.apps.AclsConfig',
|
'acls.apps.AclsConfig',
|
||||||
|
'notifications',
|
||||||
'common.apps.CommonConfig',
|
'common.apps.CommonConfig',
|
||||||
'jms_oidc_rp',
|
'jms_oidc_rp',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
|
@ -125,7 +126,7 @@ SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
||||||
# 自定义的配置,SESSION_EXPIRE_AT_BROWSER_CLOSE 始终为 True, 下面这个来控制是否强制关闭后过期 cookie
|
# 自定义的配置,SESSION_EXPIRE_AT_BROWSER_CLOSE 始终为 True, 下面这个来控制是否强制关闭后过期 cookie
|
||||||
SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE = CONFIG.SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE
|
SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE = CONFIG.SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE
|
||||||
SESSION_SAVE_EVERY_REQUEST = CONFIG.SESSION_SAVE_EVERY_REQUEST
|
SESSION_SAVE_EVERY_REQUEST = CONFIG.SESSION_SAVE_EVERY_REQUEST
|
||||||
SESSION_ENGINE = 'redis_sessions.session'
|
SESSION_ENGINE = 'jumpserver.rewriting.session'
|
||||||
SESSION_REDIS = {
|
SESSION_REDIS = {
|
||||||
'host': CONFIG.REDIS_HOST,
|
'host': CONFIG.REDIS_HOST,
|
||||||
'port': CONFIG.REDIS_PORT,
|
'port': CONFIG.REDIS_PORT,
|
||||||
|
|
|
@ -125,3 +125,6 @@ FORGOT_PASSWORD_URL = CONFIG.FORGOT_PASSWORD_URL
|
||||||
# 自定义默认组织名
|
# 自定义默认组织名
|
||||||
GLOBAL_ORG_DISPLAY_NAME = CONFIG.GLOBAL_ORG_DISPLAY_NAME
|
GLOBAL_ORG_DISPLAY_NAME = CONFIG.GLOBAL_ORG_DISPLAY_NAME
|
||||||
HEALTH_CHECK_TOKEN = CONFIG.HEALTH_CHECK_TOKEN
|
HEALTH_CHECK_TOKEN = CONFIG.HEALTH_CHECK_TOKEN
|
||||||
|
|
||||||
|
TERMINAL_RDP_ADDR = CONFIG.TERMINAL_RDP_ADDR
|
||||||
|
SECURITY_LUNA_REMEMBER_AUTH = CONFIG.SECURITY_LUNA_REMEMBER_AUTH
|
||||||
|
|
|
@ -23,6 +23,7 @@ api_v1 = [
|
||||||
path('applications/', include('applications.urls.api_urls', namespace='api-applications')),
|
path('applications/', include('applications.urls.api_urls', namespace='api-applications')),
|
||||||
path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')),
|
path('tickets/', include('tickets.urls.api_urls', namespace='api-tickets')),
|
||||||
path('acls/', include('acls.urls.api_urls', namespace='api-acls')),
|
path('acls/', include('acls.urls.api_urls', namespace='api-acls')),
|
||||||
|
path('notifications/', include('notifications.urls.notifications', namespace='api-notifications')),
|
||||||
path('prometheus/metrics/', api.PrometheusMetricsApi.as_view()),
|
path('prometheus/metrics/', api.PrometheusMetricsApi.as_view()),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,2 @@
|
||||||
|
from .notifications import *
|
||||||
|
from .site_msgs import *
|
|
@ -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)
|
|
@ -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 SiteMessage
|
||||||
|
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 = SiteMessage.get_user_all_msgs(user.id)
|
||||||
|
else:
|
||||||
|
msgs = SiteMessage.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 = SiteMessage.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']
|
||||||
|
SiteMessage.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)
|
||||||
|
SiteMessage.send_msg(**seri.validated_data, sender=request.user)
|
||||||
|
return Response({'detail': 'ok'})
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationsConfig(AppConfig):
|
||||||
|
name = 'notifications'
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -0,0 +1,14 @@
|
||||||
|
from notifications.site_msg import SiteMessage 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
|
|
@ -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)
|
|
@ -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',)
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,2 @@
|
||||||
|
from .notification import *
|
||||||
|
from .site_msg import *
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,141 @@
|
||||||
|
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:
|
||||||
|
msg = self.get_common_msg()
|
||||||
|
return {
|
||||||
|
'subject': msg,
|
||||||
|
'message': 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
|
|
@ -0,0 +1,2 @@
|
||||||
|
from .notifications import *
|
||||||
|
from .site_msgs import *
|
|
@ -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)
|
|
@ -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)
|
|
@ -0,0 +1,85 @@
|
||||||
|
from django.db.models import F
|
||||||
|
|
||||||
|
from common.utils.timezone import now
|
||||||
|
from users.models import User
|
||||||
|
from .models import SiteMessage as SiteMessageModel, SiteMessageUsers
|
||||||
|
|
||||||
|
|
||||||
|
class SiteMessage:
|
||||||
|
|
||||||
|
@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')
|
||||||
|
|
||||||
|
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):
|
||||||
|
sitemsg_users = SiteMessageUsers.objects.filter(
|
||||||
|
user_id=user_id, sitemessage_id__in=msg_ids,
|
||||||
|
has_read=False
|
||||||
|
)
|
||||||
|
|
||||||
|
for sitemsg_user in sitemsg_users:
|
||||||
|
sitemsg_user.has_read = True
|
||||||
|
sitemsg_user.read_at = now()
|
||||||
|
|
||||||
|
SiteMessageUsers.objects.bulk_update(
|
||||||
|
sitemsg_users, fields=('has_read', 'read_at'))
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
|
@ -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
|
|
@ -13,4 +13,5 @@ class OpsConfig(AppConfig):
|
||||||
from orgs.utils import set_current_org
|
from orgs.utils import set_current_org
|
||||||
set_current_org(Organization.root())
|
set_current_org(Organization.root())
|
||||||
from .celery import signal_handler
|
from .celery import signal_handler
|
||||||
|
from . import notifications
|
||||||
super().ready()
|
super().ready()
|
||||||
|
|
|
@ -9,7 +9,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils.translation import ugettext
|
from django.utils.translation import ugettext
|
||||||
from django.db import models
|
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 common.utils import lazyproperty
|
||||||
from orgs.models import Organization
|
from orgs.models import Organization
|
||||||
from orgs.mixins.models import OrgModelMixin
|
from orgs.mixins.models import OrgModelMixin
|
||||||
|
@ -99,12 +99,12 @@ class CommandExecution(OrgModelMixin):
|
||||||
else:
|
else:
|
||||||
msg = _("Command `{}` is forbidden ........").format(self.command)
|
msg = _("Command `{}` is forbidden ........").format(self.command)
|
||||||
print('\033[31m' + msg + '\033[0m')
|
print('\033[31m' + msg + '\033[0m')
|
||||||
send_command_execution_alert_mail({
|
CommandExecutionAlert({
|
||||||
'input': self.command,
|
'input': self.command,
|
||||||
'assets': self.hosts.all(),
|
'assets': self.hosts.all(),
|
||||||
'user': str(self.user),
|
'user': str(self.user),
|
||||||
'risk_level': 5,
|
'risk_level': 5,
|
||||||
})
|
}).publish_async()
|
||||||
self.result = {"error": msg}
|
self.result = {"error": msg}
|
||||||
self.org_id = self.run_as.org_id
|
self.org_id = self.run_as.org_id
|
||||||
self.is_finished = True
|
self.is_finished = True
|
||||||
|
|
|
@ -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)
|
|
@ -20,7 +20,7 @@ from .celery.utils import (
|
||||||
disable_celery_periodic_task, delete_celery_periodic_task
|
disable_celery_periodic_task, delete_celery_periodic_task
|
||||||
)
|
)
|
||||||
from .models import Task, CommandExecution, CeleryTask
|
from .models import Task, CommandExecution, CeleryTask
|
||||||
from .utils import send_server_performance_mail
|
from .notifications import ServerPerformanceMessage
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
@ -143,7 +143,7 @@ def check_server_performance_period():
|
||||||
if path.startswith(uncheck_path):
|
if path.startswith(uncheck_path):
|
||||||
need_check = False
|
need_check = False
|
||||||
if need_check and usage.percent > 80:
|
if need_check and usage.percent > 80:
|
||||||
send_server_performance_mail(path, usage, usages)
|
ServerPerformanceMessage(path=path, usage=usage).publish()
|
||||||
|
|
||||||
|
|
||||||
@shared_task(queue="ansible")
|
@shared_task(queue="ansible")
|
||||||
|
|
|
@ -69,16 +69,6 @@ def update_or_create_ansible_task(
|
||||||
return task, created
|
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):
|
def get_task_log_path(base_path, task_id, level=2):
|
||||||
task_id = str(task_id)
|
task_id = str(task_id)
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -48,7 +48,6 @@ class OrgViewSet(BulkModelViewSet):
|
||||||
queryset = Organization.objects.all()
|
queryset = Organization.objects.all()
|
||||||
serializer_class = OrgSerializer
|
serializer_class = OrgSerializer
|
||||||
permission_classes = (IsSuperUserOrAppUser,)
|
permission_classes = (IsSuperUserOrAppUser,)
|
||||||
org = None
|
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
mapper = {
|
mapper = {
|
||||||
|
@ -58,32 +57,36 @@ class OrgViewSet(BulkModelViewSet):
|
||||||
return mapper.get(self.action, super().get_serializer_class())
|
return mapper.get(self.action, super().get_serializer_class())
|
||||||
|
|
||||||
@tmp_to_root_org()
|
@tmp_to_root_org()
|
||||||
def get_data_from_model(self, model):
|
def get_data_from_model(self, org, model):
|
||||||
if model == User:
|
if model == User:
|
||||||
data = model.objects.filter(
|
data = model.objects.filter(
|
||||||
orgs__id=self.org.id,
|
orgs__id=org.id, m2m_org_members__role__in=[ROLE.USER, ROLE.ADMIN, ROLE.AUDITOR]
|
||||||
m2m_org_members__role__in=[ROLE.USER, ROLE.ADMIN, ROLE.AUDITOR]
|
|
||||||
)
|
)
|
||||||
elif model == Node:
|
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:
|
else:
|
||||||
data = model.objects.filter(org_id=self.org.id)
|
data = model.objects.filter(org_id=org.id)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
def allow_bulk_destroy(self, qs, filtered):
|
||||||
self.org = self.get_object()
|
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:
|
for model in org_related_models:
|
||||||
data = self.get_data_from_model(model)
|
data = self.get_data_from_model(instance, model)
|
||||||
if data:
|
if not data:
|
||||||
msg = _('Have {} exists, Please delete').format(model._meta.verbose_name)
|
continue
|
||||||
return Response(data={'error': msg}, status=status.HTTP_403_FORBIDDEN)
|
msg = _(
|
||||||
else:
|
'The organization have resource ({}) cannot be deleted'
|
||||||
if str(current_org) == str(self.org):
|
).format(model._meta.verbose_name)
|
||||||
msg = _('The current organization cannot be deleted')
|
raise PermissionDenied(detail=msg)
|
||||||
return Response(data={'error': msg}, status=status.HTTP_403_FORBIDDEN)
|
|
||||||
self.org.delete()
|
super().perform_destroy(instance)
|
||||||
return Response({'msg': True}, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
|
|
||||||
class OrgMemberRelationBulkViewSet(JMSBulkRelationModelViewSet):
|
class OrgMemberRelationBulkViewSet(JMSBulkRelationModelViewSet):
|
||||||
|
|
|
@ -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))
|
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)
|
_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)
|
|
||||||
|
|
|
@ -115,6 +115,7 @@ class PublicSettingApi(generics.RetrieveAPIView):
|
||||||
"OLD_PASSWORD_HISTORY_LIMIT_COUNT": settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT,
|
"OLD_PASSWORD_HISTORY_LIMIT_COUNT": settings.OLD_PASSWORD_HISTORY_LIMIT_COUNT,
|
||||||
"SECURITY_COMMAND_EXECUTION": settings.SECURITY_COMMAND_EXECUTION,
|
"SECURITY_COMMAND_EXECUTION": settings.SECURITY_COMMAND_EXECUTION,
|
||||||
"SECURITY_PASSWORD_EXPIRATION_TIME": settings.SECURITY_PASSWORD_EXPIRATION_TIME,
|
"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(),
|
"XPACK_LICENSE_IS_VALID": has_valid_xpack_license(),
|
||||||
"LOGIN_TITLE": self.get_login_title(),
|
"LOGIN_TITLE": self.get_login_title(),
|
||||||
"LOGO_URLS": self.get_logo_urls(),
|
"LOGO_URLS": self.get_logo_urls(),
|
||||||
|
|
|
@ -13,8 +13,9 @@ __all__ = [
|
||||||
class BasicSettingSerializer(serializers.Serializer):
|
class BasicSettingSerializer(serializers.Serializer):
|
||||||
SITE_URL = serializers.URLField(
|
SITE_URL = serializers.URLField(
|
||||||
required=True, label=_("Site url"),
|
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(
|
USER_GUIDE_URL = serializers.URLField(
|
||||||
required=False, allow_blank=True, allow_null=True, label=_("User guide url"),
|
required=False, allow_blank=True, allow_null=True, label=_("User guide url"),
|
||||||
help_text=_('User first login update profile done redirect to it')
|
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')
|
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_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):
|
class SecuritySettingSerializer(serializers.Serializer):
|
||||||
|
|
|
@ -4,28 +4,24 @@ import time
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.shortcuts import HttpResponse
|
from django.shortcuts import HttpResponse
|
||||||
from rest_framework import viewsets
|
|
||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
from rest_framework.fields import DateTimeField
|
from rest_framework.fields import DateTimeField
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.decorators import action
|
|
||||||
from django.template import loader
|
from django.template import loader
|
||||||
|
|
||||||
from common.http import is_true
|
from terminal.models import CommandStorage
|
||||||
from terminal.models import CommandStorage, Command
|
|
||||||
from terminal.filters import CommandFilter
|
from terminal.filters import CommandFilter
|
||||||
from orgs.utils import current_org
|
from orgs.utils import current_org
|
||||||
from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor, IsAppUser
|
from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor, IsAppUser
|
||||||
from common.const.http import GET
|
|
||||||
from common.drf.api import JMSBulkModelViewSet
|
from common.drf.api import JMSBulkModelViewSet
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from terminal.utils import send_command_alert_mail
|
|
||||||
from terminal.serializers import InsecureCommandAlertSerializer
|
from terminal.serializers import InsecureCommandAlertSerializer
|
||||||
from terminal.exceptions import StorageInvalid
|
from terminal.exceptions import StorageInvalid
|
||||||
from ..backends import (
|
from ..backends import (
|
||||||
get_command_storage, get_multi_command_storage,
|
get_command_storage, get_multi_command_storage,
|
||||||
SessionCommandSerializer,
|
SessionCommandSerializer,
|
||||||
)
|
)
|
||||||
|
from ..notifications import CommandAlertMessage
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
__all__ = ['CommandViewSet', 'CommandExportApi', 'InsecureCommandAlertAPI']
|
__all__ = ['CommandViewSet', 'CommandExportApi', 'InsecureCommandAlertAPI']
|
||||||
|
@ -211,5 +207,5 @@ class InsecureCommandAlertAPI(generics.CreateAPIView):
|
||||||
if command['risk_level'] >= settings.SECURITY_INSECURE_COMMAND_LEVEL and \
|
if command['risk_level'] >= settings.SECURITY_INSECURE_COMMAND_LEVEL and \
|
||||||
settings.SECURITY_INSECURE_COMMAND and \
|
settings.SECURITY_INSECURE_COMMAND and \
|
||||||
settings.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER:
|
settings.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER:
|
||||||
send_command_alert_mail(command)
|
CommandAlertMessage(command).publish_async()
|
||||||
return Response()
|
return Response()
|
||||||
|
|
|
@ -10,4 +10,5 @@ class TerminalConfig(AppConfig):
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from . import signals_handler
|
from . import signals_handler
|
||||||
|
from . import notifications
|
||||||
return super().ready()
|
return super().ready()
|
||||||
|
|
|
@ -16,6 +16,7 @@ class ReplayStorageTypeChoices(TextChoices):
|
||||||
swift = 'swift', 'Swift'
|
swift = 'swift', 'Swift'
|
||||||
oss = 'oss', 'OSS'
|
oss = 'oss', 'OSS'
|
||||||
azure = 'azure', 'Azure'
|
azure = 'azure', 'Azure'
|
||||||
|
obs = 'obs', 'OBS'
|
||||||
|
|
||||||
|
|
||||||
class CommandStorageTypeChoices(TextChoices):
|
class CommandStorageTypeChoices(TextChoices):
|
||||||
|
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -109,8 +109,11 @@ class Session(OrgModelMixin):
|
||||||
_PROTOCOL = self.PROTOCOL
|
_PROTOCOL = self.PROTOCOL
|
||||||
if self.is_finished:
|
if self.is_finished:
|
||||||
return False
|
return False
|
||||||
|
if self.login_from == self.LOGIN_FROM.RT:
|
||||||
|
return False
|
||||||
if self.protocol in [
|
if self.protocol in [
|
||||||
_PROTOCOL.SSH, _PROTOCOL.VNC, _PROTOCOL.RDP, _PROTOCOL.TELNET, _PROTOCOL.K8S
|
_PROTOCOL.SSH, _PROTOCOL.VNC, _PROTOCOL.RDP,
|
||||||
|
_PROTOCOL.TELNET, _PROTOCOL.K8S
|
||||||
]:
|
]:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -0,0 +1,142 @@
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from users.models import User
|
||||||
|
from common.utils import get_logger, reverse
|
||||||
|
from notifications.notifications import SystemMessage
|
||||||
|
from terminal.models import Session, Command
|
||||||
|
from notifications.models import SystemMsgSubscription
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
__all__ = ('CommandAlertMessage', 'CommandExecutionAlert')
|
||||||
|
|
||||||
|
CATEGORY = 'terminal'
|
||||||
|
CATEGORY_LABEL = _('Terminal')
|
||||||
|
|
||||||
|
|
||||||
|
class CommandAlertMixin:
|
||||||
|
@classmethod
|
||||||
|
def post_insert_to_db(cls, subscription: SystemMsgSubscription):
|
||||||
|
"""
|
||||||
|
兼容操作,试图用 `settings.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER` 的邮件地址找到
|
||||||
|
用户,把用户设置为默认接收者
|
||||||
|
"""
|
||||||
|
emails = settings.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER.split(',')
|
||||||
|
emails = [email.strip() for email in emails]
|
||||||
|
|
||||||
|
users = User.objects.filter(email__in=emails)
|
||||||
|
subscription.users.add(*users)
|
||||||
|
|
||||||
|
|
||||||
|
class CommandAlertMessage(CommandAlertMixin, SystemMessage):
|
||||||
|
category = CATEGORY
|
||||||
|
category_label = CATEGORY_LABEL
|
||||||
|
message_type_label = _('Terminal command alert')
|
||||||
|
|
||||||
|
def __init__(self, command):
|
||||||
|
self.command = command
|
||||||
|
|
||||||
|
def _get_message(self):
|
||||||
|
command = self.command
|
||||||
|
session_obj = Session.objects.get(id=command['session'])
|
||||||
|
|
||||||
|
message = _("""
|
||||||
|
Command: %(command)s
|
||||||
|
<br>
|
||||||
|
Asset: %(host_name)s (%(host_ip)s)
|
||||||
|
<br>
|
||||||
|
User: %(user)s
|
||||||
|
<br>
|
||||||
|
Level: %(risk_level)s
|
||||||
|
<br>
|
||||||
|
Session: <a href="%(session_detail_url)s">session detail</a>
|
||||||
|
<br>
|
||||||
|
""") % {
|
||||||
|
'command': command['input'],
|
||||||
|
'host_name': command['asset'],
|
||||||
|
'host_ip': session_obj.asset_obj.ip,
|
||||||
|
'user': command['user'],
|
||||||
|
'risk_level': Command.get_risk_level_str(command['risk_level']),
|
||||||
|
'session_detail_url': reverse('api-terminal:session-detail',
|
||||||
|
kwargs={'pk': command['session']},
|
||||||
|
external=True, api_to_ui=True),
|
||||||
|
}
|
||||||
|
|
||||||
|
return message
|
||||||
|
|
||||||
|
def get_common_msg(self):
|
||||||
|
return self._get_message()
|
||||||
|
|
||||||
|
def get_email_msg(self):
|
||||||
|
command = self.command
|
||||||
|
session_obj = Session.objects.get(id=command['session'])
|
||||||
|
|
||||||
|
input = command['input']
|
||||||
|
if isinstance(input, str):
|
||||||
|
input = input.replace('\r\n', ' ').replace('\r', ' ').replace('\n', ' ')
|
||||||
|
|
||||||
|
subject = _("Insecure Command Alert: [%(name)s->%(login_from)s@%(remote_addr)s] $%(command)s") % {
|
||||||
|
'name': command['user'],
|
||||||
|
'login_from': session_obj.get_login_from_display(),
|
||||||
|
'remote_addr': session_obj.remote_addr,
|
||||||
|
'command': input
|
||||||
|
}
|
||||||
|
|
||||||
|
message = self._get_message(command)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'subject': subject,
|
||||||
|
'message': message
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CommandExecutionAlert(CommandAlertMixin, SystemMessage):
|
||||||
|
category = CATEGORY
|
||||||
|
category_label = CATEGORY_LABEL
|
||||||
|
message_type_label = _('Batch command alert')
|
||||||
|
|
||||||
|
def __init__(self, command):
|
||||||
|
self.command = command
|
||||||
|
|
||||||
|
def _get_message(self):
|
||||||
|
command = self.command
|
||||||
|
input = command['input']
|
||||||
|
input = input.replace('\n', '<br>')
|
||||||
|
|
||||||
|
assets = ', '.join([str(asset) for asset in command['assets']])
|
||||||
|
message = _("""
|
||||||
|
<br>
|
||||||
|
Assets: %(assets)s
|
||||||
|
<br>
|
||||||
|
User: %(user)s
|
||||||
|
<br>
|
||||||
|
Level: %(risk_level)s
|
||||||
|
<br>
|
||||||
|
|
||||||
|
----------------- Commands ---------------- <br>
|
||||||
|
%(command)s <br>
|
||||||
|
----------------- Commands ---------------- <br>
|
||||||
|
""") % {
|
||||||
|
'command': input,
|
||||||
|
'assets': assets,
|
||||||
|
'user': command['user'],
|
||||||
|
'risk_level': Command.get_risk_level_str(command['risk_level']),
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
|
||||||
|
def get_common_msg(self):
|
||||||
|
return self._get_message()
|
||||||
|
|
||||||
|
def get_email_msg(self):
|
||||||
|
command = self.command
|
||||||
|
|
||||||
|
subject = _("Insecure Web Command Execution Alert: [%(name)s]") % {
|
||||||
|
'name': command['user'],
|
||||||
|
}
|
||||||
|
message = self._get_message(command)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'subject': subject,
|
||||||
|
'message': message
|
||||||
|
}
|
|
@ -82,6 +82,16 @@ class ReplayStorageTypeOSSSerializer(ReplayStorageTypeBaseSerializer):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ReplayStorageTypeOBSSerializer(ReplayStorageTypeBaseSerializer):
|
||||||
|
endpoint_help_text = '''
|
||||||
|
OBS format: obs.{REGION_NAME}.myhuaweicloud.com
|
||||||
|
Such as: obs.cn-north-4.myhuaweicloud.com
|
||||||
|
'''
|
||||||
|
ENDPOINT = serializers.CharField(
|
||||||
|
max_length=1024, label=_('Endpoint'), help_text=_(endpoint_help_text), allow_null=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ReplayStorageTypeAzureSerializer(serializers.Serializer):
|
class ReplayStorageTypeAzureSerializer(serializers.Serializer):
|
||||||
class EndpointSuffixChoices(TextChoices):
|
class EndpointSuffixChoices(TextChoices):
|
||||||
china = 'core.chinacloudapi.cn', 'core.chinacloudapi.cn'
|
china = 'core.chinacloudapi.cn', 'core.chinacloudapi.cn'
|
||||||
|
@ -105,7 +115,8 @@ replay_storage_type_serializer_classes_mapping = {
|
||||||
const.ReplayStorageTypeChoices.ceph.value: ReplayStorageTypeCephSerializer,
|
const.ReplayStorageTypeChoices.ceph.value: ReplayStorageTypeCephSerializer,
|
||||||
const.ReplayStorageTypeChoices.swift.value: ReplayStorageTypeSwiftSerializer,
|
const.ReplayStorageTypeChoices.swift.value: ReplayStorageTypeSwiftSerializer,
|
||||||
const.ReplayStorageTypeChoices.oss.value: ReplayStorageTypeOSSSerializer,
|
const.ReplayStorageTypeChoices.oss.value: ReplayStorageTypeOSSSerializer,
|
||||||
const.ReplayStorageTypeChoices.azure.value: ReplayStorageTypeAzureSerializer
|
const.ReplayStorageTypeChoices.azure.value: ReplayStorageTypeAzureSerializer,
|
||||||
|
const.ReplayStorageTypeChoices.obs.value: ReplayStorageTypeOBSSerializer
|
||||||
}
|
}
|
||||||
|
|
||||||
# ReplayStorageSerializer
|
# ReplayStorageSerializer
|
||||||
|
|
|
@ -68,78 +68,6 @@ def get_session_replay_url(session):
|
||||||
return local_path, url
|
return local_path, url
|
||||||
|
|
||||||
|
|
||||||
def send_command_alert_mail(command):
|
|
||||||
session_obj = Session.objects.get(id=command['session'])
|
|
||||||
|
|
||||||
input = command['input']
|
|
||||||
if isinstance(input, str):
|
|
||||||
input = input.replace('\r\n', ' ').replace('\r', ' ').replace('\n', ' ')
|
|
||||||
|
|
||||||
subject = _("Insecure Command Alert: [%(name)s->%(login_from)s@%(remote_addr)s] $%(command)s") % {
|
|
||||||
'name': command['user'],
|
|
||||||
'login_from': session_obj.get_login_from_display(),
|
|
||||||
'remote_addr': session_obj.remote_addr,
|
|
||||||
'command': input
|
|
||||||
}
|
|
||||||
|
|
||||||
recipient_list = settings.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER.split(',')
|
|
||||||
message = _("""
|
|
||||||
Command: %(command)s
|
|
||||||
<br>
|
|
||||||
Asset: %(host_name)s (%(host_ip)s)
|
|
||||||
<br>
|
|
||||||
User: %(user)s
|
|
||||||
<br>
|
|
||||||
Level: %(risk_level)s
|
|
||||||
<br>
|
|
||||||
Session: <a href="%(session_detail_url)s">session detail</a>
|
|
||||||
<br>
|
|
||||||
""") % {
|
|
||||||
'command': command['input'],
|
|
||||||
'host_name': command['asset'],
|
|
||||||
'host_ip': session_obj.asset_obj.ip,
|
|
||||||
'user': command['user'],
|
|
||||||
'risk_level': Command.get_risk_level_str(command['risk_level']),
|
|
||||||
'session_detail_url': reverse('api-terminal:session-detail',
|
|
||||||
kwargs={'pk': command['session']},
|
|
||||||
external=True, api_to_ui=True),
|
|
||||||
}
|
|
||||||
logger.debug(message)
|
|
||||||
|
|
||||||
send_mail_async.delay(subject, message, recipient_list, html_message=message)
|
|
||||||
|
|
||||||
|
|
||||||
def send_command_execution_alert_mail(command):
|
|
||||||
subject = _("Insecure Web Command Execution Alert: [%(name)s]") % {
|
|
||||||
'name': command['user'],
|
|
||||||
}
|
|
||||||
input = command['input']
|
|
||||||
input = input.replace('\n', '<br>')
|
|
||||||
recipient_list = settings.SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER.split(',')
|
|
||||||
|
|
||||||
assets = ', '.join([str(asset) for asset in command['assets']])
|
|
||||||
message = _("""
|
|
||||||
<br>
|
|
||||||
Assets: %(assets)s
|
|
||||||
<br>
|
|
||||||
User: %(user)s
|
|
||||||
<br>
|
|
||||||
Level: %(risk_level)s
|
|
||||||
<br>
|
|
||||||
|
|
||||||
----------------- Commands ---------------- <br>
|
|
||||||
%(command)s <br>
|
|
||||||
----------------- Commands ---------------- <br>
|
|
||||||
""") % {
|
|
||||||
'command': input,
|
|
||||||
'assets': assets,
|
|
||||||
'user': command['user'],
|
|
||||||
'risk_level': Command.get_risk_level_str(command['risk_level']),
|
|
||||||
}
|
|
||||||
|
|
||||||
send_mail_async.delay(subject, message, recipient_list, html_message=message)
|
|
||||||
|
|
||||||
|
|
||||||
class ComputeStatUtil:
|
class ComputeStatUtil:
|
||||||
# system status
|
# system status
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.1 on 2021-05-26 03:00
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0034_auto_20210506_1448'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='need_update_password',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Need update password'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -599,13 +599,21 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
|
||||||
auto_now_add=True, blank=True, null=True,
|
auto_now_add=True, blank=True, null=True,
|
||||||
verbose_name=_('Date password last updated')
|
verbose_name=_('Date password last updated')
|
||||||
)
|
)
|
||||||
need_update_password = models.BooleanField(default=False)
|
need_update_password = models.BooleanField(
|
||||||
|
default=False, verbose_name=_('Need update password')
|
||||||
|
)
|
||||||
wecom_id = models.CharField(null=True, default=None, unique=True, max_length=128)
|
wecom_id = models.CharField(null=True, default=None, unique=True, max_length=128)
|
||||||
dingtalk_id = models.CharField(null=True, default=None, unique=True, max_length=128)
|
dingtalk_id = models.CharField(null=True, default=None, unique=True, max_length=128)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '{0.name}({0.username})'.format(self)
|
return '{0.name}({0.username})'.format(self)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_group_ids_by_user_id(cls, user_id):
|
||||||
|
group_ids = cls.groups.through.objects.filter(user_id=user_id).distinct().values_list('usergroup_id', flat=True)
|
||||||
|
group_ids = list(group_ids)
|
||||||
|
return group_ids
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_wecom_bound(self):
|
def is_wecom_bound(self):
|
||||||
return bool(self.wecom_id)
|
return bool(self.wecom_id)
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
#
|
#
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.db.models import TextChoices
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from common.mixins import CommonBulkSerializerMixin
|
from common.mixins import CommonBulkSerializerMixin
|
||||||
|
@ -17,15 +18,13 @@ __all__ = [
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer):
|
class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer):
|
||||||
EMAIL_SET_PASSWORD = _('Reset link will be generated and sent to the user')
|
class PasswordStrategy(TextChoices):
|
||||||
CUSTOM_PASSWORD = _('Set password')
|
email = 'email', _('Reset link will be generated and sent to the user')
|
||||||
PASSWORD_STRATEGY_CHOICES = (
|
custom = 'custom', _('Set password')
|
||||||
(0, EMAIL_SET_PASSWORD),
|
|
||||||
(1, CUSTOM_PASSWORD)
|
|
||||||
)
|
|
||||||
password_strategy = serializers.ChoiceField(
|
password_strategy = serializers.ChoiceField(
|
||||||
choices=PASSWORD_STRATEGY_CHOICES, required=False,
|
choices=PasswordStrategy.choices, default=PasswordStrategy.email, required=False,
|
||||||
label=_('Password strategy'), write_only=True, default=0
|
write_only=True, label=_('Password strategy')
|
||||||
)
|
)
|
||||||
mfa_enabled = serializers.BooleanField(read_only=True, label=_('MFA enabled'))
|
mfa_enabled = serializers.BooleanField(read_only=True, label=_('MFA enabled'))
|
||||||
mfa_force_enabled = serializers.BooleanField(read_only=True, label=_('MFA force enabled'))
|
mfa_force_enabled = serializers.BooleanField(read_only=True, label=_('MFA force enabled'))
|
||||||
|
@ -117,9 +116,11 @@ class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer):
|
||||||
def validate_password(self, password):
|
def validate_password(self, password):
|
||||||
from ..utils import check_password_rules
|
from ..utils import check_password_rules
|
||||||
password_strategy = self.initial_data.get('password_strategy')
|
password_strategy = self.initial_data.get('password_strategy')
|
||||||
if password_strategy == '0':
|
if self.instance is None and password_strategy != self.PasswordStrategy.custom:
|
||||||
|
# 创建用户,使用邮件设置密码
|
||||||
return
|
return
|
||||||
if password_strategy is None and not password:
|
if self.instance and not password:
|
||||||
|
# 更新用户, 未设置密码
|
||||||
return
|
return
|
||||||
if not check_password_rules(password):
|
if not check_password_rules(password):
|
||||||
msg = _('Password does not match security rules')
|
msg = _('Password does not match security rules')
|
||||||
|
|
|
@ -62,7 +62,7 @@ pytz==2018.3
|
||||||
PyYAML==5.1
|
PyYAML==5.1
|
||||||
redis==3.5.3
|
redis==3.5.3
|
||||||
requests==2.22.0
|
requests==2.22.0
|
||||||
jms-storage==0.0.35
|
jms-storage==0.0.37
|
||||||
s3transfer==0.3.3
|
s3transfer==0.3.3
|
||||||
simplejson==3.13.2
|
simplejson==3.13.2
|
||||||
six==1.11.0
|
six==1.11.0
|
||||||
|
|
Loading…
Reference in New Issue