Merge pull request #6256 from jumpserver/dev

v2.11.0 rc1
pull/6269/head
Jiangjie.Bai 2021-06-10 14:03:45 +08:00 committed by GitHub
commit 1e8d9ba2ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
89 changed files with 2300 additions and 502 deletions

1
.gitignore vendored
View File

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

View File

@ -265,6 +265,7 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
- [极速安装](https://docs.jumpserver.org/zh/master/install/setup_by_fast/)
- [完整文档](https://docs.jumpserver.org)
- [演示视频](https://www.bilibili.com/video/BV1ZV41127GB)
- [手动安装](https://github.com/jumpserver/installer)
## 组件项目
- [Lina](https://github.com/jumpserver/lina) JumpServer Web UI 项目

View File

@ -2,18 +2,49 @@
#
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 ..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):
model = models.Application
model = Application
filterset_fields = ('name', 'type', 'category')
search_fields = filterset_fields
permission_classes = (IsOrgAdminOrAppUser,)
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

View File

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

View File

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

View File

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

View File

@ -106,6 +106,7 @@ class DBBackend(BaseBackend):
class SystemUserBackend(DBBackend):
model = SystemUser.assets.through
backend = 'system_user'
backend_display = _('System user')
prefer = backend
base_score = 0
union_id_length = 2
@ -138,6 +139,7 @@ class SystemUserBackend(DBBackend):
kwargs = dict(
hostname=F("asset__hostname"),
ip=F("asset__ip"),
name=F("systemuser__name"),
username=F("systemuser__username"),
password=F("systemuser__password"),
private_key=F("systemuser__private_key"),
@ -152,7 +154,8 @@ class SystemUserBackend(DBBackend):
union_id=Concat(F("systemuser_id"), Value("_"), F("asset_id"),
output_field=CharField()),
org_id=F("asset__org_id"),
backend=Value(self.backend, CharField())
backend=Value(self.backend, CharField()),
backend_display=Value(self.backend_display, CharField()),
)
return kwargs
@ -174,12 +177,17 @@ class SystemUserBackend(DBBackend):
class DynamicSystemUserBackend(SystemUserBackend):
backend = 'system_user_dynamic'
backend_display = _('System user(Dynamic)')
prefer = 'system_user'
union_id_length = 3
def get_annotate(self):
kwargs = super().get_annotate()
kwargs.update(dict(
name=Concat(
F("systemuser__users__name"), Value('('), F("systemuser__name"), Value(')'),
output_field=CharField()
),
username=F("systemuser__users__username"),
asset_username=Concat(
F("asset__id"), Value("_"),
@ -221,6 +229,7 @@ class DynamicSystemUserBackend(SystemUserBackend):
class AdminUserBackend(DBBackend):
model = Asset
backend = 'admin_user'
backend_display = _('Admin user')
prefer = backend
base_score = 200
@ -246,6 +255,7 @@ class AdminUserBackend(DBBackend):
def all(self):
qs = self.model.objects.all().annotate(
asset_id=F("id"),
name=F("admin_user__name"),
username=F("admin_user__username"),
password=F("admin_user__password"),
private_key=F("admin_user__private_key"),
@ -256,6 +266,7 @@ class AdminUserBackend(DBBackend):
asset_username=Concat(F("id"), Value("_"), F("admin_user__username"), output_field=CharField()),
union_id=Concat(F("admin_user_id"), Value("_"), F("id"), output_field=CharField()),
backend=Value(self.backend, CharField()),
backend_display=Value(self.backend_display, CharField()),
)
qs = self.qs_to_values(qs)
return qs
@ -264,6 +275,7 @@ class AdminUserBackend(DBBackend):
class AuthbookBackend(DBBackend):
model = AuthBook
backend = 'db'
backend_display = _('Database')
prefer = backend
base_score = 400
@ -313,6 +325,7 @@ class AuthbookBackend(DBBackend):
asset_username=Concat(F("asset__id"), Value("_"), F("username"), output_field=CharField()),
union_id=Concat(F("id"), Value("_"), F("asset_id"), output_field=CharField()),
backend=Value(self.backend, CharField()),
backend_display=Value(self.backend_display, CharField()),
)
qs = self.qs_to_values(qs)
return qs

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework import serializers
from common.utils import get_logger, random_string
from common.drf.api import SerializerMixin2
@ -49,7 +50,7 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
raise PermissionDenied(error)
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:
raise PermissionDenied('Connection token disabled')
if not user:
@ -79,7 +80,7 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
})
key = self.CACHE_KEY_PREFIX.format(token)
cache.set(key, value, timeout=30*60)
cache.set(key, value, timeout=ttl)
return token
def create(self, request, *args, **kwargs):
@ -93,14 +94,14 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
token = self.create_token(user, asset, application, system_user)
return Response({"token": token}, status=201)
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file')
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file', permission_classes=[IsValidUser])
def get_rdp_file(self, request, *args, **kwargs):
options = {
'full address:s': '',
'username:s': '',
'screen mode id:i': '0',
'desktopwidth:i': '1280',
'desktopheight:i': '800',
# 'desktopwidth:i': '1280',
# 'desktopheight:i': '800',
'use multimon:i': '1',
'session bpp:i': '32',
'audiomode:i': '0',
@ -120,6 +121,8 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
'autoreconnection enabled:i': '1',
'bookmarktype:i': '3',
'use redirection server name:i': '0',
'smart sizing:i': '0',
# 'domain:s': ''
# 'alternate shell:s:': '||MySQLWorkbench',
# 'remoteapplicationname:s': 'Firefox',
# 'remoteapplicationcmdline:s': '',
@ -134,17 +137,23 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
asset = serializer.validated_data.get('asset')
application = serializer.validated_data.get('application')
system_user = serializer.validated_data['system_user']
user = serializer.validated_data.get('user')
height = serializer.validated_data.get('height')
width = serializer.validated_data.get('width')
user = request.user
token = self.create_token(user, asset, application, system_user)
# Todo: 上线后地址是 JumpServerAddr:3389
address = self.request.query_params.get('address') or '1.1.1.1'
address = settings.TERMINAL_RDP_ADDR
if not address or address == 'localhost:3389':
address = request.get_host().split(':')[0] + ':3389'
options['full address:s'] = address
options['username:s'] = '{}|{}'.format(user.username, token)
if system_user.ad_domain:
options['domain:s'] = system_user.ad_domain
if width and height:
options['desktopwidth:i'] = width
options['desktopheight:i'] = height
else:
options['smart sizing:i'] = '1'
data = ''
for k, v in options.items():
data += f'{k}:{v}\n'
@ -155,10 +164,8 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
return response
@staticmethod
def _get_application_secret_detail(value):
from applications.models import Application
def _get_application_secret_detail(application):
from perms.models import Action
application = get_object_or_404(Application, id=value.get('application'))
gateway = None
if not application.category_remote_app:
@ -184,15 +191,15 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
}
@staticmethod
def _get_asset_secret_detail(value, user, system_user):
from assets.models import Asset
def _get_asset_secret_detail(asset, user, system_user):
from perms.utils.asset import get_asset_system_user_ids_with_actions_by_user
asset = get_object_or_404(Asset, id=value.get('asset'))
systemuserid_actions_mapper = get_asset_system_user_ids_with_actions_by_user(user, asset)
actions = systemuserid_actions_mapper.get(system_user.id, [])
gateway = None
if asset and asset.domain and asset.domain.has_gateway():
gateway = asset.domain.random_gateway()
return {
'asset': asset,
'application': None,
@ -201,26 +208,47 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
'actions': actions,
}
@action(methods=['POST'], detail=False, permission_classes=[IsSuperUserOrAppUser], url_path='secret-info/detail')
def get_secret_detail(self, request, *args, **kwargs):
def valid_token(self, token):
from users.models import User
from assets.models import SystemUser
from assets.models import SystemUser, Asset
from applications.models import Application
token = request.data.get('token', '')
key = self.CACHE_KEY_PREFIX.format(token)
value = cache.get(key, None)
if not value:
return Response(status=404)
user = get_object_or_404(User, id=value.get('user'))
system_user = get_object_or_404(SystemUser, id=value.get('system_user'))
data = dict(user=user, system_user=system_user)
raise serializers.ValidationError('Token not found')
user = get_object_or_404(User, id=value.get('user'))
if not user.is_valid:
raise serializers.ValidationError("User not valid, disabled or expired")
system_user = get_object_or_404(SystemUser, id=value.get('system_user'))
asset = None
app = None
if value.get('type') == 'asset':
asset_detail = self._get_asset_secret_detail(value, user=user, system_user=system_user)
asset = get_object_or_404(Asset, id=value.get('asset'))
else:
app = get_object_or_404(Application, id=value.get('application'))
if asset and not asset.is_active:
raise serializers.ValidationError("Asset disabled")
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.update(asset_detail)
else:
app_detail = self._get_application_secret_detail(value)
app_detail = self._get_application_secret_detail(app)
system_user.load_app_more_auth(app.id, user.id)
data['type'] = 'application'
data.update(app_detail)

View File

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

View File

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

View File

@ -94,7 +94,7 @@ class BaseFileParser(BaseParser):
new_row_data = {}
serializer_fields = self.serializer_fields
for k, v in row_data.items():
if isinstance(v, list) or isinstance(v, dict) or isinstance(v, str) and k.strip() and v.strip():
if type(v) in [list, dict, int] or (isinstance(v, str) and k.strip() and v.strip()):
# 解决类似disk_info为字符串的'{}'的问题
if not isinstance(v, str) and isinstance(serializer_fields[k], serializers.CharField):
v = str(v)

View File

@ -259,6 +259,7 @@ class Config(dict):
'SECURITY_INSECURE_COMMAND': False,
'SECURITY_INSECURE_COMMAND_LEVEL': 5,
'SECURITY_INSECURE_COMMAND_EMAIL_RECEIVER': '',
'SECURITY_LUNA_REMEMBER_AUTH': True,
'HTTP_BIND_HOST': '0.0.0.0',
'HTTP_LISTEN_PORT': 8080,
@ -300,7 +301,9 @@ class Config(dict):
'SESSION_SAVE_EVERY_REQUEST': True,
'SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE': False,
'FORGOT_PASSWORD_URL': '',
'HEALTH_CHECK_TOKEN': ''
'HEALTH_CHECK_TOKEN': '',
'TERMINAL_RDP_ADDR': ''
}
def compatible_auth_openid_of_key(self):

View File

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

View File

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

View File

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

View File

@ -0,0 +1,58 @@
from rest_framework.response import Response
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
from rest_framework.decorators import action
from common.http import is_true
from common.permissions import IsValidUser
from common.const.http import GET, PATCH, POST
from common.drf.api import JmsGenericViewSet
from ..serializers import (
SiteMessageDetailSerializer, SiteMessageIdsSerializer,
SiteMessageSendSerializer,
)
from ..site_msg import 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'})

View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class NotificationsConfig(AppConfig):
name = 'notifications'

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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'))

View File

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

View File

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.6 on 2021-06-04 03:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('terminal', '0035_auto_20210517_1448'),
]
operations = [
migrations.AlterField(
model_name='replaystorage',
name='type',
field=models.CharField(choices=[('null', 'Null'), ('server', 'Server'), ('s3', 'S3'), ('ceph', 'Ceph'), ('swift', 'Swift'), ('oss', 'OSS'), ('azure', 'Azure'), ('obs', 'OBS')], default='server', max_length=16, verbose_name='Type'),
),
]

View File

@ -109,8 +109,11 @@ class Session(OrgModelMixin):
_PROTOCOL = self.PROTOCOL
if self.is_finished:
return False
if self.login_from == self.LOGIN_FROM.RT:
return False
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
else:

View File

@ -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
}

View File

@ -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 EndpointSuffixChoices(TextChoices):
china = 'core.chinacloudapi.cn', 'core.chinacloudapi.cn'
@ -105,7 +115,8 @@ replay_storage_type_serializer_classes_mapping = {
const.ReplayStorageTypeChoices.ceph.value: ReplayStorageTypeCephSerializer,
const.ReplayStorageTypeChoices.swift.value: ReplayStorageTypeSwiftSerializer,
const.ReplayStorageTypeChoices.oss.value: ReplayStorageTypeOSSSerializer,
const.ReplayStorageTypeChoices.azure.value: ReplayStorageTypeAzureSerializer
const.ReplayStorageTypeChoices.azure.value: ReplayStorageTypeAzureSerializer,
const.ReplayStorageTypeChoices.obs.value: ReplayStorageTypeOBSSerializer
}
# ReplayStorageSerializer

View File

@ -68,78 +68,6 @@ def get_session_replay_url(session):
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:
# system status
@staticmethod

View File

@ -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'),
),
]

View File

@ -599,13 +599,21 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
auto_now_add=True, blank=True, null=True,
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)
dingtalk_id = models.CharField(null=True, default=None, unique=True, max_length=128)
def __str__(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
def is_wecom_bound(self):
return bool(self.wecom_id)

View File

@ -2,6 +2,7 @@
#
from django.core.cache import cache
from django.utils.translation import ugettext_lazy as _
from django.db.models import TextChoices
from rest_framework import serializers
from common.mixins import CommonBulkSerializerMixin
@ -17,15 +18,13 @@ __all__ = [
class UserSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer):
EMAIL_SET_PASSWORD = _('Reset link will be generated and sent to the user')
CUSTOM_PASSWORD = _('Set password')
PASSWORD_STRATEGY_CHOICES = (
(0, EMAIL_SET_PASSWORD),
(1, CUSTOM_PASSWORD)
)
class PasswordStrategy(TextChoices):
email = 'email', _('Reset link will be generated and sent to the user')
custom = 'custom', _('Set password')
password_strategy = serializers.ChoiceField(
choices=PASSWORD_STRATEGY_CHOICES, required=False,
label=_('Password strategy'), write_only=True, default=0
choices=PasswordStrategy.choices, default=PasswordStrategy.email, required=False,
write_only=True, label=_('Password strategy')
)
mfa_enabled = serializers.BooleanField(read_only=True, label=_('MFA 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):
from ..utils import check_password_rules
password_strategy = self.initial_data.get('password_strategy')
if password_strategy == '0':
if self.instance is None and password_strategy != self.PasswordStrategy.custom:
# 创建用户,使用邮件设置密码
return
if password_strategy is None and not password:
if self.instance and not password:
# 更新用户, 未设置密码
return
if not check_password_rules(password):
msg = _('Password does not match security rules')

View File

@ -62,7 +62,7 @@ pytz==2018.3
PyYAML==5.1
redis==3.5.3
requests==2.22.0
jms-storage==0.0.35
jms-storage==0.0.37
s3transfer==0.3.3
simplejson==3.13.2
six==1.11.0