Merge pull request #6795 from jumpserver/dev

v2.14.0 rc1
pull/6892/head
Jiangjie.Bai 2021-09-09 21:18:32 +08:00 committed by GitHub
commit e993e7257c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
174 changed files with 6003 additions and 2917 deletions

View File

@ -22,10 +22,13 @@ COPY ./requirements/deb_buster_requirements.txt ./requirements/deb_buster_requir
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \ RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \ && sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& apt update \ && apt update \
&& apt -y install telnet iproute2 redis-tools \
&& grep -v '^#' ./requirements/deb_buster_requirements.txt | xargs apt -y install \ && grep -v '^#' ./requirements/deb_buster_requirements.txt | xargs apt -y install \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& localedef -c -f UTF-8 -i zh_CN zh_CN.UTF-8 \ && localedef -c -f UTF-8 -i zh_CN zh_CN.UTF-8 \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& sed -i "s@# alias l@alias l@g" ~/.bashrc \
&& echo "set mouse-=a" > ~/.vimrc
COPY ./requirements/requirements.txt ./requirements/requirements.txt COPY ./requirements/requirements.txt ./requirements/requirements.txt
RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \ RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \
@ -37,6 +40,12 @@ COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
RUN mkdir -p /root/.ssh/ \ RUN mkdir -p /root/.ssh/ \
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config && echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config
RUN mkdir -p /opt/jumpserver/oracle/
ADD https://f2c-north-rel.oss-cn-qingdao.aliyuncs.com/2.0/north/jumpserver/instantclient-basiclite-linux.x64-21.1.0.0.0.tar /opt/jumpserver/oracle/
RUN tar xvf /opt/jumpserver/oracle/instantclient-basiclite-linux.x64-21.1.0.0.0.tar -C /opt/jumpserver/oracle/
RUN sh -c "echo /opt/jumpserver/oracle/instantclient_21_1 > /etc/ld.so.conf.d/oracle-instantclient.conf"
RUN ldconfig
RUN echo > config.yml RUN echo > config.yml
VOLUME /opt/jumpserver/data VOLUME /opt/jumpserver/data
VOLUME /opt/jumpserver/logs VOLUME /opt/jumpserver/logs

View File

@ -83,11 +83,11 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
@classmethod @classmethod
def create_login_asset_confirm_ticket(cls, user, asset, system_user, assignees, org_id): def create_login_asset_confirm_ticket(cls, user, asset, system_user, assignees, org_id):
from tickets.const import TicketTypeChoices from tickets.const import TicketType
from tickets.models import Ticket from tickets.models import Ticket
data = { data = {
'title': _('Login asset confirm') + ' ({})'.format(user), 'title': _('Login asset confirm') + ' ({})'.format(user),
'type': TicketTypeChoices.login_asset_confirm, 'type': TicketType.login_asset_confirm,
'meta': { 'meta': {
'apply_login_user': str(user), 'apply_login_user': str(user),
'apply_login_asset': str(asset), 'apply_login_asset': str(asset),
@ -96,7 +96,7 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
'org_id': org_id, 'org_id': org_id,
} }
ticket = Ticket.objects.create(**data) ticket = Ticket.objects.create(**data)
ticket.assignees.set(assignees) ticket.create_process_map_and_node(assignees)
ticket.open(applicant=user) ticket.open(applicant=user)
return ticket return ticket

View File

@ -2,74 +2,57 @@
# #
from django_filters import rest_framework as filters from django_filters import rest_framework as filters
from django.db.models import F, Value, CharField from django.db.models import F, Q
from django.db.models.functions import Concat
from django.http import Http404
from common.drf.filters import BaseFilterSet from common.drf.filters import BaseFilterSet
from common.drf.api import JMSModelViewSet from common.drf.api import JMSBulkModelViewSet
from common.utils import unique from ..models import Account
from perms.models import ApplicationPermission
from ..hands import IsOrgAdminOrAppUser, IsOrgAdmin, NeedMFAVerify from ..hands import IsOrgAdminOrAppUser, IsOrgAdmin, NeedMFAVerify
from .. import serializers from .. import serializers
class AccountFilterSet(BaseFilterSet): class AccountFilterSet(BaseFilterSet):
username = filters.CharFilter(field_name='username') username = filters.CharFilter(field_name='username')
app = filters.CharFilter(field_name='applications', lookup_expr='exact') type = filters.CharFilter(field_name='type', lookup_expr='exact')
app_name = filters.CharFilter(field_name='app_name', lookup_expr='exact') category = filters.CharFilter(field_name='category', lookup_expr='exact')
app_display = filters.CharFilter(field_name='app_display', lookup_expr='exact')
class Meta: class Meta:
model = ApplicationPermission model = Account
fields = ['type', 'category'] fields = ['app', 'systemuser']
@property
def qs(self):
qs = super().qs
qs = self.filter_username(qs)
return qs
def filter_username(self, qs):
username = self.get_query_param('username')
if not username:
return qs
qs = qs.filter(Q(username=username) | Q(systemuser__username=username)).distinct()
return qs
class ApplicationAccountViewSet(JMSModelViewSet): class ApplicationAccountViewSet(JMSBulkModelViewSet):
permission_classes = (IsOrgAdmin, ) model = Account
search_fields = ['username', 'app_name'] search_fields = ['username', 'app_display']
filterset_class = AccountFilterSet filterset_class = AccountFilterSet
filterset_fields = ['username', 'app_name', 'type', 'category'] filterset_fields = ['username', 'app_display', 'type', 'category', 'app']
serializer_class = serializers.ApplicationAccountSerializer serializer_class = serializers.AppAccountSerializer
http_method_names = ['get', 'put', 'patch', 'options'] permission_classes = (IsOrgAdmin,)
def get_queryset(self): def get_queryset(self):
queryset = ApplicationPermission.objects\ queryset = Account.objects.all() \
.exclude(system_users__isnull=True) \ .annotate(type=F('app__type')) \
.exclude(applications__isnull=True) \ .annotate(app_display=F('app__name')) \
.annotate(uid=Concat( .annotate(systemuser_display=F('systemuser__name')) \
'applications', Value('_'), 'system_users', output_field=CharField() .annotate(category=F('app__category'))
)) \
.annotate(systemuser=F('system_users')) \
.annotate(systemuser_display=F('system_users__name')) \
.annotate(username=F('system_users__username')) \
.annotate(password=F('system_users__password')) \
.annotate(app=F('applications')) \
.annotate(app_name=F("applications__name")) \
.values('username', 'password', 'systemuser', 'systemuser_display',
'app', 'app_name', 'category', 'type', 'uid', 'org_id')
return queryset
def get_object(self):
obj = self.get_queryset().filter(
uid=self.kwargs['pk']
).first()
if not obj:
raise Http404()
return obj
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
queryset_list = unique(queryset, key=lambda x: (x['app'], x['systemuser']))
return queryset_list
@staticmethod
def filter_spm_queryset(resource_ids, queryset):
queryset = queryset.filter(uid__in=resource_ids)
return queryset return queryset
class ApplicationAccountSecretViewSet(ApplicationAccountViewSet): class ApplicationAccountSecretViewSet(ApplicationAccountViewSet):
serializer_class = serializers.ApplicationAccountSecretSerializer serializer_class = serializers.AppAccountSecretSerializer
permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify] permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
http_method_names = ['get', 'options'] http_method_names = ['get', 'options']

View File

@ -6,11 +6,10 @@ from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from common.tree import TreeNodeSerializer from common.tree import TreeNodeSerializer
from ..hands import IsOrgAdminOrAppUser from ..hands import IsOrgAdminOrAppUser, IsValidUser
from .. import serializers from .. import serializers
from ..models import Application from ..models import Application
__all__ = ['ApplicationViewSet'] __all__ = ['ApplicationViewSet']
@ -24,7 +23,7 @@ class ApplicationViewSet(OrgBulkModelViewSet):
search_fields = ('name', 'type', 'category') search_fields = ('name', 'type', 'category')
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
serializer_classes = { serializer_classes = {
'default': serializers.ApplicationSerializer, 'default': serializers.AppSerializer,
'get_tree': TreeNodeSerializer 'get_tree': TreeNodeSerializer
} }
@ -35,3 +34,9 @@ class ApplicationViewSet(OrgBulkModelViewSet):
tree_nodes = Application.create_tree_nodes(queryset, show_count=show_count) tree_nodes = Application.create_tree_nodes(queryset, show_count=show_count)
serializer = self.get_serializer(tree_nodes, many=True) serializer = self.get_serializer(tree_nodes, many=True)
return Response(serializer.data) return Response(serializer.data)
@action(methods=['get'], detail=False, permission_classes=(IsValidUser,))
def suggestion(self, request):
queryset = self.filter_queryset(self.get_queryset())[:3]
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)

View File

@ -0,0 +1,76 @@
# Generated by Django 3.1.12 on 2021-08-26 09:07
import assets.models.base
import common.fields.model
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import simple_history.models
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('assets', '0076_delete_assetuser'),
('applications', '0009_applicationuser'),
]
operations = [
migrations.CreateModel(
name='HistoricalAccount',
fields=[
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')),
('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')),
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
('version', models.IntegerField(default=1, verbose_name='Version')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField()),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('app', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='applications.application', verbose_name='Database')),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('systemuser', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='assets.systemuser', verbose_name='System user')),
],
options={
'verbose_name': 'historical Account',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': 'history_date',
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='Account',
fields=[
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
('version', models.IntegerField(default=1, verbose_name='Version')),
('app', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='applications.application', verbose_name='Database')),
('systemuser', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.systemuser', verbose_name='System user')),
],
options={
'verbose_name': 'Account',
'unique_together': {('username', 'app', 'systemuser')},
},
bases=(models.Model, assets.models.base.AuthMixin),
),
]

View File

@ -0,0 +1,40 @@
# Generated by Django 3.1.12 on 2021-08-26 09:59
from django.db import migrations, transaction
from django.db.models import F
def migrate_app_account(apps, schema_editor):
db_alias = schema_editor.connection.alias
app_perm_model = apps.get_model("perms", "ApplicationPermission")
app_account_model = apps.get_model("applications", 'Account')
queryset = app_perm_model.objects \
.exclude(system_users__isnull=True) \
.exclude(applications__isnull=True) \
.annotate(systemuser=F('system_users')) \
.annotate(app=F('applications')) \
.values('app', 'systemuser', 'org_id')
accounts = []
for p in queryset:
if not p['app']:
continue
account = app_account_model(
app_id=p['app'], systemuser_id=p['systemuser'],
version=1, org_id=p['org_id']
)
accounts.append(account)
app_account_model.objects.using(db_alias).bulk_create(accounts, ignore_conflicts=True)
class Migration(migrations.Migration):
dependencies = [
('applications', '0010_appaccount_historicalappaccount'),
]
operations = [
migrations.RunPython(migrate_app_account)
]

View File

@ -1 +1,2 @@
from .application import * from .application import *
from .account import *

View File

@ -0,0 +1,88 @@
from django.db import models
from simple_history.models import HistoricalRecords
from django.utils.translation import ugettext_lazy as _
from common.utils import lazyproperty
from assets.models.base import BaseUser
class Account(BaseUser):
app = models.ForeignKey('applications.Application', on_delete=models.CASCADE, null=True, verbose_name=_('Database'))
systemuser = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE, null=True, verbose_name=_("System user"))
version = models.IntegerField(default=1, verbose_name=_('Version'))
history = HistoricalRecords()
auth_attrs = ['username', 'password', 'private_key', 'public_key']
class Meta:
verbose_name = _('Account')
unique_together = [('username', 'app', 'systemuser')]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.auth_snapshot = {}
def get_or_systemuser_attr(self, attr):
val = getattr(self, attr, None)
if val:
return val
if self.systemuser:
return getattr(self.systemuser, attr, '')
return ''
def load_auth(self):
for attr in self.auth_attrs:
value = self.get_or_systemuser_attr(attr)
self.auth_snapshot[attr] = [getattr(self, attr), value]
setattr(self, attr, value)
def unload_auth(self):
if not self.systemuser:
return
for attr, values in self.auth_snapshot.items():
origin_value, loaded_value = values
current_value = getattr(self, attr, '')
if current_value == loaded_value:
setattr(self, attr, origin_value)
def save(self, *args, **kwargs):
self.unload_auth()
instance = super().save(*args, **kwargs)
self.load_auth()
return instance
@lazyproperty
def category(self):
return self.app.category
@lazyproperty
def type(self):
return self.app.type
@lazyproperty
def app_display(self):
return self.systemuser.name
@property
def username_display(self):
return self.get_or_systemuser_attr('username') or ''
@lazyproperty
def systemuser_display(self):
if not self.systemuser:
return ''
return str(self.systemuser)
@property
def smart_name(self):
username = self.username_display
if self.app:
app = str(self.app)
else:
app = '*'
return '{}@{}'.format(username, app)
def __str__(self):
return self.smart_name

View File

@ -4,20 +4,23 @@
from rest_framework import serializers from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orgs.models import Organization
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from assets.serializers.base import AuthSerializerMixin
from common.drf.serializers import MethodSerializer from common.drf.serializers import MethodSerializer
from .attrs import category_serializer_classes_mapping, type_serializer_classes_mapping from .attrs import (
category_serializer_classes_mapping,
type_serializer_classes_mapping
)
from .. import models from .. import models
from .. import const from .. import const
__all__ = [ __all__ = [
'ApplicationSerializer', 'ApplicationSerializerMixin', 'AppSerializer', 'AppSerializerMixin',
'ApplicationAccountSerializer', 'ApplicationAccountSecretSerializer' 'AppAccountSerializer', 'AppAccountSecretSerializer'
] ]
class ApplicationSerializerMixin(serializers.Serializer): class AppSerializerMixin(serializers.Serializer):
attrs = MethodSerializer() attrs = MethodSerializer()
def get_attrs_serializer(self): def get_attrs_serializer(self):
@ -45,8 +48,14 @@ class ApplicationSerializerMixin(serializers.Serializer):
serializer = serializer_class serializer = serializer_class
return serializer return serializer
def create(self, validated_data):
return super().create(validated_data)
class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSerializer): def update(self, instance, validated_data):
return super().update(instance, validated_data)
class AppSerializer(AppSerializerMixin, BulkOrgResourceModelSerializer):
category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category display')) category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category display'))
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display')) type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
@ -69,42 +78,54 @@ class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSeri
return _attrs return _attrs
class ApplicationAccountSerializer(serializers.Serializer): class AppAccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
id = serializers.ReadOnlyField(label=_("Id"), source='uid')
username = serializers.ReadOnlyField(label=_("Username"))
password = serializers.CharField(write_only=True, label=_("Password"))
systemuser = serializers.ReadOnlyField(label=_('System user'))
systemuser_display = serializers.ReadOnlyField(label=_("System user display"))
app = serializers.ReadOnlyField(label=_('App'))
app_name = serializers.ReadOnlyField(label=_("Application name"), read_only=True)
category = serializers.ChoiceField(label=_('Category'), choices=const.AppCategory.choices, read_only=True) category = serializers.ChoiceField(label=_('Category'), choices=const.AppCategory.choices, read_only=True)
category_display = serializers.SerializerMethodField(label=_('Category display')) category_display = serializers.SerializerMethodField(label=_('Category display'))
type = serializers.ChoiceField(label=_('Type'), choices=const.AppType.choices, read_only=True) type = serializers.ChoiceField(label=_('Type'), choices=const.AppType.choices, read_only=True)
type_display = serializers.SerializerMethodField(label=_('Type display')) type_display = serializers.SerializerMethodField(label=_('Type display'))
uid = serializers.ReadOnlyField(label=_("Union id"))
org_id = serializers.ReadOnlyField(label=_("Organization"))
org_name = serializers.SerializerMethodField(label=_("Org name"))
category_mapper = dict(const.AppCategory.choices) category_mapper = dict(const.AppCategory.choices)
type_mapper = dict(const.AppType.choices) type_mapper = dict(const.AppType.choices)
def create(self, validated_data): class Meta:
pass model = models.Account
fields_mini = ['id', 'username', 'version']
def update(self, instance, validated_data): fields_write_only = ['password', 'private_key']
pass fields_fk = ['systemuser', 'systemuser_display', 'app', 'app_display']
fields = fields_mini + fields_fk + fields_write_only + [
'type', 'type_display', 'category', 'category_display',
]
extra_kwargs = {
'username': {'default': '', 'required': False},
'password': {'write_only': True},
'app_display': {'label': _('Application display')}
}
use_model_bulk_create = True
model_bulk_create_kwargs = {
'ignore_conflicts': True
}
def get_category_display(self, obj): def get_category_display(self, obj):
return self.category_mapper.get(obj['category']) return self.category_mapper.get(obj.category)
def get_type_display(self, obj): def get_type_display(self, obj):
return self.type_mapper.get(obj['type']) return self.type_mapper.get(obj.type)
@staticmethod @classmethod
def get_org_name(obj): def setup_eager_loading(cls, queryset):
org = Organization.get_instance(obj['org_id']) """ Perform necessary eager loading of data. """
return org.name queryset = queryset.prefetch_related('systemuser', 'app')
return queryset
def to_representation(self, instance):
instance.load_auth()
return super().to_representation(instance)
class ApplicationAccountSecretSerializer(ApplicationAccountSerializer): class AppAccountSecretSerializer(AppAccountSerializer):
password = serializers.CharField(write_only=False, label=_("Password")) class Meta(AppAccountSerializer.Meta):
extra_kwargs = {
'password': {'write_only': False},
'private_key': {'write_only': False},
'public_key': {'write_only': False},
}

View File

@ -64,8 +64,8 @@ class AccountViewSet(OrgBulkModelViewSet):
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset()\ queryset = super().get_queryset() \
.annotate(ip=F('asset__ip'))\ .annotate(ip=F('asset__ip')) \
.annotate(hostname=F('asset__hostname')) .annotate(hostname=F('asset__hostname'))
return queryset return queryset
@ -110,4 +110,5 @@ class AccountTaskCreateAPI(CreateAPIView):
def get_exception_handler(self): def get_exception_handler(self):
def handler(e, context): def handler(e, context):
return Response({"error": str(e)}, status=400) return Response({"error": str(e)}, status=400)
return handler return handler

View File

@ -1,15 +1,17 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from assets.api import FilterAssetByNodeMixin from assets.api import FilterAssetByNodeMixin
from rest_framework.decorators import action
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from rest_framework.generics import RetrieveAPIView from rest_framework.generics import RetrieveAPIView
from rest_framework.response import Response
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from common.utils import get_logger, get_object_or_none from common.utils import get_logger, get_object_or_none
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsSuperUser from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsSuperUser, IsValidUser
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics from orgs.mixins import generics
from ..models import Asset, Node, Platform, SystemUser from ..models import Asset, Node, Platform
from .. import serializers from .. import serializers
from ..tasks import ( from ..tasks import (
update_assets_hardware_info_manual, test_assets_connectivity_manual, update_assets_hardware_info_manual, test_assets_connectivity_manual,
@ -17,7 +19,6 @@ from ..tasks import (
) )
from ..filters import FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend from ..filters import FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = [ __all__ = [
'AssetViewSet', 'AssetPlatformRetrieveApi', 'AssetViewSet', 'AssetPlatformRetrieveApi',
@ -43,6 +44,7 @@ class AssetViewSet(FilterAssetByNodeMixin, OrgBulkModelViewSet):
ordering_fields = ("hostname", "ip", "port", "cpu_cores") ordering_fields = ("hostname", "ip", "port", "cpu_cores")
serializer_classes = { serializer_classes = {
'default': serializers.AssetSerializer, 'default': serializers.AssetSerializer,
'suggestion': serializers.MiniAssetSerializer
} }
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
extra_filter_backends = [FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend] extra_filter_backends = [FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend]
@ -62,6 +64,12 @@ class AssetViewSet(FilterAssetByNodeMixin, OrgBulkModelViewSet):
assets = serializer.save() assets = serializer.save()
self.set_assets_node(assets) self.set_assets_node(assets)
@action(methods=['get'], detail=False, permission_classes=(IsValidUser,))
def suggestion(self, request):
queryset = self.filter_queryset(self.get_queryset())[:3]
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
class AssetPlatformRetrieveApi(RetrieveAPIView): class AssetPlatformRetrieveApi(RetrieveAPIView):
queryset = Platform.objects.all() queryset = Platform.objects.all()

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from collections import defaultdict from collections import defaultdict
from django.db.models import F, Value from django.db.models import F, Value, Model
from django.db.models.signals import m2m_changed from django.db.models.signals import m2m_changed
from django.db.models.functions import Concat from django.db.models.functions import Concat
@ -13,13 +13,15 @@ from .. import models, serializers
__all__ = [ __all__ = [
'SystemUserAssetRelationViewSet', 'SystemUserNodeRelationViewSet', 'SystemUserAssetRelationViewSet', 'SystemUserNodeRelationViewSet',
'SystemUserUserRelationViewSet', 'SystemUserUserRelationViewSet', 'BaseRelationViewSet',
] ]
logger = get_logger(__name__) logger = get_logger(__name__)
class RelationMixin: class RelationMixin:
model: Model
def get_queryset(self): def get_queryset(self):
queryset = self.model.objects.all() queryset = self.model.objects.all()
if not current_org.is_root(): if not current_org.is_root():

View File

@ -1,7 +1,8 @@
# Generated by Django 3.1.6 on 2021-06-04 16:46 # Generated by Django 3.1.6 on 2021-06-04 16:46
import uuid
from django.db import migrations, models, transaction from django.db import migrations, models, transaction
import django.db.models.deletion import django.db.models.deletion
from django.db import IntegrityError
from django.db.models import F from django.db.models import F
@ -15,7 +16,7 @@ def migrate_admin_user_to_system_user(apps, schema_editor):
for admin_user in admin_users: for admin_user in admin_users:
kwargs = {} kwargs = {}
for attr in [ for attr in [
'id', 'org_id', 'username', 'password', 'private_key', 'public_key', 'org_id', 'username', 'password', 'private_key', 'public_key',
'comment', 'date_created', 'date_updated', 'created_by', 'comment', 'date_created', 'date_updated', 'created_by',
]: ]:
value = getattr(admin_user, attr) value = getattr(admin_user, attr)
@ -27,7 +28,16 @@ def migrate_admin_user_to_system_user(apps, schema_editor):
).exists() ).exists()
if exist: if exist:
name = admin_user.name + '_' + str(admin_user.id)[:5] name = admin_user.name + '_' + str(admin_user.id)[:5]
i = admin_user.id
exist = system_user_model.objects.using(db_alias).filter(
id=i, org_id=admin_user.org_id
).exists()
if exist:
i = uuid.uuid4()
kwargs.update({ kwargs.update({
'id': i,
'name': name, 'name': name,
'type': 'admin', 'type': 'admin',
'protocol': 'ssh', 'protocol': 'ssh',
@ -36,7 +46,11 @@ def migrate_admin_user_to_system_user(apps, schema_editor):
with transaction.atomic(): with transaction.atomic():
s = system_user_model(**kwargs) s = system_user_model(**kwargs)
s.save() try:
s.save()
except IntegrityError:
s.id = None
s.save()
print(" Migrate admin user to system user: {} => {}".format(admin_user.name, s.name)) print(" Migrate admin user to system user: {} => {}".format(admin_user.name, s.name))
assets = admin_user.assets.all() assets = admin_user.assets.all()
s.assets.set(assets) s.assets.set(assets)

View File

@ -16,7 +16,6 @@ class AuthBook(BaseUser, AbsConnectivity):
systemuser = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE, null=True, verbose_name=_("System user")) systemuser = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE, null=True, verbose_name=_("System user"))
version = models.IntegerField(default=1, verbose_name=_('Version')) version = models.IntegerField(default=1, verbose_name=_('Version'))
history = HistoricalRecords() history = HistoricalRecords()
_systemuser_display = ''
auth_attrs = ['username', 'password', 'private_key', 'public_key'] auth_attrs = ['username', 'password', 'private_key', 'public_key']
@ -64,8 +63,6 @@ class AuthBook(BaseUser, AbsConnectivity):
@lazyproperty @lazyproperty
def systemuser_display(self): def systemuser_display(self):
if self._systemuser_display:
return self._systemuser_display
if not self.systemuser: if not self.systemuser:
return '' return ''
return str(self.systemuser) return str(self.systemuser)

View File

@ -105,11 +105,11 @@ class CommandFilterRule(OrgModelMixin):
return '{} % {}'.format(self.type, self.content) return '{} % {}'.format(self.type, self.content)
def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id): def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id):
from tickets.const import TicketTypeChoices from tickets.const import TicketType
from tickets.models import Ticket from tickets.models import Ticket
data = { data = {
'title': _('Command confirm') + ' ({})'.format(session.user), 'title': _('Command confirm') + ' ({})'.format(session.user),
'type': TicketTypeChoices.command_confirm, 'type': TicketType.command_confirm,
'meta': { 'meta': {
'apply_run_user': session.user, 'apply_run_user': session.user,
'apply_run_asset': session.asset, 'apply_run_asset': session.asset,
@ -122,6 +122,6 @@ class CommandFilterRule(OrgModelMixin):
'org_id': org_id, 'org_id': org_id,
} }
ticket = Ticket.objects.create(**data) ticket = Ticket.objects.create(**data)
ticket.assignees.set(self.reviewers.all()) ticket.create_process_map_and_node(self.reviewers.all())
ticket.open(applicant=session.user_obj) ticket.open(applicant=session.user_obj)
return ticket return ticket

View File

@ -73,6 +73,10 @@ class ProtocolMixin:
def can_perm_to_asset(self): def can_perm_to_asset(self):
return self.protocol in self.ASSET_CATEGORY_PROTOCOLS return self.protocol in self.ASSET_CATEGORY_PROTOCOLS
@property
def is_asset_protocol(self):
return self.protocol in self.ASSET_CATEGORY_PROTOCOLS
class AuthMixin: class AuthMixin:
username_same_with_user: bool username_same_with_user: bool

View File

@ -8,7 +8,7 @@ from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import Asset, Node, Platform, SystemUser from ..models import Asset, Node, Platform, SystemUser
__all__ = [ __all__ = [
'AssetSerializer', 'AssetSimpleSerializer', 'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer',
'ProtocolsField', 'PlatformSerializer', 'ProtocolsField', 'PlatformSerializer',
'AssetTaskSerializer', 'AssetsTaskSerializer', 'ProtocolsField' 'AssetTaskSerializer', 'AssetsTaskSerializer', 'ProtocolsField'
] ]
@ -69,6 +69,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
""" """
资产的数据结构 资产的数据结构
""" """
class Meta: class Meta:
model = Asset model = Asset
fields_mini = ['id', 'hostname', 'ip', 'platform', 'protocols'] fields_mini = ['id', 'hostname', 'ip', 'platform', 'protocols']
@ -157,6 +158,12 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
return instance return instance
class MiniAssetSerializer(serializers.ModelSerializer):
class Meta:
model = Asset
fields = AssetSerializer.Meta.fields_mini
class PlatformSerializer(serializers.ModelSerializer): class PlatformSerializer(serializers.ModelSerializer):
meta = serializers.DictField(required=False, allow_null=True, label=_('Meta')) meta = serializers.DictField(required=False, allow_null=True, label=_('Meta'))
@ -177,7 +184,6 @@ class PlatformSerializer(serializers.ModelSerializer):
class AssetSimpleSerializer(serializers.ModelSerializer): class AssetSimpleSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Asset model = Asset
fields = ['id', 'hostname', 'ip', 'port', 'connectivity', 'date_verified'] fields = ['id', 'hostname', 'ip', 'port', 'connectivity', 'date_verified']

View File

@ -14,7 +14,7 @@ __all__ = [
'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer', 'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer',
'SystemUserNodeRelationSerializer', 'SystemUserTaskSerializer', 'SystemUserNodeRelationSerializer', 'SystemUserTaskSerializer',
'SystemUserUserRelationSerializer', 'SystemUserWithAuthInfoSerializer', 'SystemUserUserRelationSerializer', 'SystemUserWithAuthInfoSerializer',
'SystemUserTempAuthSerializer', 'SystemUserTempAuthSerializer', 'RelationMixin',
] ]
@ -31,12 +31,12 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
fields_mini = ['id', 'name', 'username'] fields_mini = ['id', 'name', 'username']
fields_write_only = ['password', 'public_key', 'private_key'] fields_write_only = ['password', 'public_key', 'private_key']
fields_small = fields_mini + fields_write_only + [ fields_small = fields_mini + fields_write_only + [
'type', 'type_display', 'protocol', 'login_mode', 'login_mode_display', 'token', 'ssh_key_fingerprint',
'priority', 'sudo', 'shell', 'sftp_root', 'token', 'ssh_key_fingerprint', 'type', 'type_display', 'protocol', 'is_asset_protocol',
'home', 'system_groups', 'ad_domain', 'login_mode', 'login_mode_display', 'priority',
'sudo', 'shell', 'sftp_root', 'home', 'system_groups', 'ad_domain',
'username_same_with_user', 'auto_push', 'auto_generate_key', 'username_same_with_user', 'auto_push', 'auto_generate_key',
'date_created', 'date_updated', 'date_created', 'date_updated', 'comment', 'created_by',
'comment', 'created_by',
] ]
fields_m2m = ['cmd_filters', 'assets_amount'] fields_m2m = ['cmd_filters', 'assets_amount']
fields = fields_small + fields_m2m fields = fields_small + fields_m2m
@ -53,6 +53,7 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
'login_mode_display': {'label': _('Login mode display')}, 'login_mode_display': {'label': _('Login mode display')},
'created_by': {'read_only': True}, 'created_by': {'read_only': True},
'ad_domain': {'required': False, 'allow_blank': True, 'label': _('Ad domain')}, 'ad_domain': {'required': False, 'allow_blank': True, 'label': _('Ad domain')},
'is_asset_protocol': {'label': _('Is asset protocol')}
} }
def validate_auto_push(self, value): def validate_auto_push(self, value):

View File

@ -131,8 +131,8 @@ def on_system_user_groups_change(instance, action, pk_set, reverse, **kwargs):
@on_transaction_commit @on_transaction_commit
def on_system_user_update(instance: SystemUser, created, **kwargs): def on_system_user_update(instance: SystemUser, created, **kwargs):
""" """
当系统用户更新时可能更新了用户名等这时要自动推送系统用户到资产上, 当系统用户更新时可能更新了用户名等这时要自动推送系统用户到资产上,
其实应该当 用户名密码sudo等更新时再推送这里偷个懒, 其实应该当 用户名密码sudo等更新时再推送这里偷个懒,
这里直接取了 instance.assets 因为nodes和系统用户发生变化时会自动将nodes下的资产 这里直接取了 instance.assets 因为nodes和系统用户发生变化时会自动将nodes下的资产
关联到上面 关联到上面
""" """

View File

@ -5,14 +5,12 @@ from django.utils import timezone
from celery import shared_task from celery import shared_task
from ops.celery.decorator import ( from ops.celery.decorator import (
register_as_period_task, after_app_shutdown_clean_periodic register_as_period_task
) )
from .models import UserLoginLog, OperateLog from .models import UserLoginLog, OperateLog
from common.utils import get_log_keep_day from common.utils import get_log_keep_day
@shared_task
@after_app_shutdown_clean_periodic
def clean_login_log_period(): def clean_login_log_period():
now = timezone.now() now = timezone.now()
days = get_log_keep_day('LOGIN_LOG_KEEP_DAYS') days = get_log_keep_day('LOGIN_LOG_KEEP_DAYS')
@ -20,8 +18,6 @@ def clean_login_log_period():
UserLoginLog.objects.filter(datetime__lt=expired_day).delete() UserLoginLog.objects.filter(datetime__lt=expired_day).delete()
@shared_task
@after_app_shutdown_clean_periodic
def clean_operation_log_period(): def clean_operation_log_period():
now = timezone.now() now = timezone.now()
days = get_log_keep_day('OPERATE_LOG_KEEP_DAYS') days = get_log_keep_day('OPERATE_LOG_KEEP_DAYS')
@ -29,7 +25,6 @@ def clean_operation_log_period():
OperateLog.objects.filter(datetime__lt=expired_day).delete() OperateLog.objects.filter(datetime__lt=expired_day).delete()
@shared_task
def clean_ftp_log_period(): def clean_ftp_log_period():
now = timezone.now() now = timezone.now()
days = get_log_keep_day('FTP_LOG_KEEP_DAYS') days = get_log_keep_day('FTP_LOG_KEEP_DAYS')

View File

@ -74,6 +74,7 @@ class ClientProtocolMixin:
'bookmarktype:i': '3', 'bookmarktype:i': '3',
'use redirection server name:i': '0', 'use redirection server name:i': '0',
'smart sizing:i': '0', 'smart sizing:i': '0',
#'drivestoredirect:s': '*',
# 'domain:s': '' # 'domain:s': ''
# 'alternate shell:s:': '||MySQLWorkbench', # 'alternate shell:s:': '||MySQLWorkbench',
# 'remoteapplicationname:s': 'Firefox', # 'remoteapplicationname:s': 'Firefox',
@ -84,8 +85,11 @@ class ClientProtocolMixin:
height = self.request.query_params.get('height') height = self.request.query_params.get('height')
width = self.request.query_params.get('width') width = self.request.query_params.get('width')
full_screen = is_true(self.request.query_params.get('full_screen')) full_screen = is_true(self.request.query_params.get('full_screen'))
drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
token = self.create_token(user, asset, application, system_user) token = self.create_token(user, asset, application, system_user)
if drives_redirect:
options['drivestoredirect:s'] = '*'
options['screen mode id:i'] = '2' if full_screen else '1' options['screen mode id:i'] = '2' if full_screen else '1'
address = settings.TERMINAL_RDP_ADDR address = settings.TERMINAL_RDP_ADDR
if not address or address == 'localhost:3389': if not address or address == 'localhost:3389':

View File

@ -45,5 +45,5 @@ class TicketStatusApi(mixins.AuthMixin, APIView):
ticket = self.get_ticket() ticket = self.get_ticket()
if ticket: if ticket:
request.session.pop('auth_ticket_id', '') request.session.pop('auth_ticket_id', '')
ticket.close(processor=request.user) ticket.close(processor=self.get_user_from_session())
return Response('', status=200) return Response('', status=200)

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import builtins
import time import time
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.conf import settings from django.conf import settings
@ -8,14 +9,28 @@ from rest_framework.generics import CreateAPIView
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from rest_framework.response import Response from rest_framework.response import Response
from common.permissions import IsValidUser, NeedMFAVerify from authentication.sms_verify_code import VerifyCodeUtil
from common.exceptions import JMSException
from common.permissions import IsValidUser, NeedMFAVerify, IsAppUser
from users.models.user import MFAType
from ..serializers import OtpVerifySerializer from ..serializers import OtpVerifySerializer
from .. import serializers from .. import serializers
from .. import errors from .. import errors
from ..mixins import AuthMixin from ..mixins import AuthMixin
__all__ = ['MFAChallengeApi', 'UserOtpVerifyApi'] __all__ = ['MFAChallengeApi', 'UserOtpVerifyApi', 'SendSMSVerifyCodeApi', 'MFASelectTypeApi']
class MFASelectTypeApi(AuthMixin, CreateAPIView):
permission_classes = (AllowAny,)
serializer_class = serializers.MFASelectTypeSerializer
def perform_create(self, serializer):
mfa_type = serializer.validated_data['type']
if mfa_type == MFAType.SMS_CODE:
user = self.get_user_from_session()
user.send_sms_code()
class MFAChallengeApi(AuthMixin, CreateAPIView): class MFAChallengeApi(AuthMixin, CreateAPIView):
@ -26,7 +41,9 @@ class MFAChallengeApi(AuthMixin, CreateAPIView):
try: try:
user = self.get_user_from_session() user = self.get_user_from_session()
code = serializer.validated_data.get('code') code = serializer.validated_data.get('code')
valid = user.check_mfa(code) mfa_type = serializer.validated_data.get('type', MFAType.OTP)
valid = user.check_mfa(code, mfa_type=mfa_type)
if not valid: if not valid:
self.request.session['auth_mfa'] = '' self.request.session['auth_mfa'] = ''
raise errors.MFAFailedError( raise errors.MFAFailedError(
@ -67,3 +84,12 @@ class UserOtpVerifyApi(CreateAPIView):
if self.request.method.lower() == 'get' and settings.SECURITY_VIEW_AUTH_NEED_MFA: if self.request.method.lower() == 'get' and settings.SECURITY_VIEW_AUTH_NEED_MFA:
self.permission_classes = [NeedMFAVerify] self.permission_classes = [NeedMFAVerify]
return super().get_permissions() return super().get_permissions()
class SendSMSVerifyCodeApi(AuthMixin, CreateAPIView):
permission_classes = (AllowAny,)
def create(self, request, *args, **kwargs):
user = self.get_user_from_session()
timeout = user.send_sms_code()
return Response({'code': 'ok','timeout': timeout})

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .backends import * from .backends import *
from .callback import *

View File

@ -1,16 +0,0 @@
# -*- coding: utf-8 -*-
#
from django.contrib.auth import get_user_model
User = get_user_model()
def cas_callback(response):
username = response['username']
user, user_created = User.objects.get_or_create(username=username)
profile, created = user.get_profile()
profile.role = response['attributes']['role']
profile.birth_date = response['attributes']['birth_date']
profile.save()

View File

@ -4,9 +4,11 @@ from django.utils.translation import ugettext_lazy as _
from django.urls import reverse from django.urls import reverse
from django.conf import settings from django.conf import settings
from authentication import sms_verify_code
from common.exceptions import JMSException from common.exceptions import JMSException
from .signals import post_auth_failed from .signals import post_auth_failed
from users.utils import LoginBlockUtil, MFABlockUtils from users.utils import LoginBlockUtil, MFABlockUtils
from users.models import MFAType
reason_password_failed = 'password_failed' reason_password_failed = 'password_failed'
reason_password_decrypt_failed = 'password_decrypt_failed' reason_password_decrypt_failed = 'password_decrypt_failed'
@ -58,8 +60,18 @@ block_mfa_msg = _(
"The account has been locked " "The account has been locked "
"(please contact admin to unlock it or try again after {} minutes)" "(please contact admin to unlock it or try again after {} minutes)"
) )
mfa_failed_msg = _( otp_failed_msg = _(
"MFA code invalid, or ntp sync server time, " "One-time password invalid, or ntp sync server time, "
"You can also try {times_try} times "
"(The account will be temporarily locked for {block_time} minutes)"
)
sms_failed_msg = _(
"SMS verify code invalid,"
"You can also try {times_try} times "
"(The account will be temporarily locked for {block_time} minutes)"
)
mfa_type_failed_msg = _(
"The MFA type({mfa_type}) is not supported"
"You can also try {times_try} times " "You can also try {times_try} times "
"(The account will be temporarily locked for {block_time} minutes)" "(The account will be temporarily locked for {block_time} minutes)"
) )
@ -134,7 +146,7 @@ class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError):
error = reason_mfa_failed error = reason_mfa_failed
msg: str msg: str
def __init__(self, username, request, ip): def __init__(self, username, request, ip, mfa_type=MFAType.OTP):
util = MFABlockUtils(username, ip) util = MFABlockUtils(username, ip)
util.incr_failed_count() util.incr_failed_count()
@ -142,9 +154,18 @@ class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError):
block_time = settings.SECURITY_LOGIN_LIMIT_TIME block_time = settings.SECURITY_LOGIN_LIMIT_TIME
if times_remainder: if times_remainder:
self.msg = mfa_failed_msg.format( if mfa_type == MFAType.OTP:
times_try=times_remainder, block_time=block_time self.msg = otp_failed_msg.format(
) times_try=times_remainder, block_time=block_time
)
elif mfa_type == MFAType.SMS_CODE:
self.msg = sms_failed_msg.format(
times_try=times_remainder, block_time=block_time
)
else:
self.msg = mfa_type_failed_msg.format(
mfa_type=mfa_type, times_try=times_remainder, block_time=block_time
)
else: else:
self.msg = block_mfa_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME) self.msg = block_mfa_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
super().__init__(username=username, request=request) super().__init__(username=username, request=request)
@ -202,12 +223,16 @@ class MFARequiredError(NeedMoreInfoError):
msg = mfa_required_msg msg = mfa_required_msg
error = 'mfa_required' error = 'mfa_required'
def __init__(self, error='', msg='', mfa_types=tuple(MFAType)):
super().__init__(error=error, msg=msg)
self.choices = mfa_types
def as_data(self): def as_data(self):
return { return {
'error': self.error, 'error': self.error,
'msg': self.msg, 'msg': self.msg,
'data': { 'data': {
'choices': ['code'], 'choices': self.choices,
'url': reverse('api-auth:mfa-challenge') 'url': reverse('api-auth:mfa-challenge')
} }
} }

View File

@ -43,7 +43,8 @@ class UserLoginForm(forms.Form):
class UserCheckOtpCodeForm(forms.Form): class UserCheckOtpCodeForm(forms.Form):
otp_code = forms.CharField(label=_('MFA code'), max_length=6) code = forms.CharField(label=_('MFA Code'), max_length=6)
mfa_type = forms.CharField(label=_('MFA type'), max_length=6)
class CustomCaptchaTextInput(CaptchaTextInput): class CustomCaptchaTextInput(CaptchaTextInput):

View File

@ -0,0 +1,14 @@
from django.shortcuts import redirect
class MFAMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
if request.path.find('/auth/login/otp/') > -1:
return response
if request.session.get('auth_mfa_required'):
return redirect('authentication:login-otp')
return response

View File

@ -14,14 +14,15 @@ from django.contrib.auth import (
PermissionDenied, user_login_failed, _clean_credentials PermissionDenied, user_login_failed, _clean_credentials
) )
from django.shortcuts import reverse, redirect from django.shortcuts import reverse, redirect
from django.views.generic.edit import FormView
from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get, FlashMessageUtil from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get, FlashMessageUtil
from users.models import User from users.models import User, MFAType
from users.utils import LoginBlockUtil, MFABlockUtils from users.utils import LoginBlockUtil, MFABlockUtils
from . import errors from . import errors
from .utils import rsa_decrypt from .utils import rsa_decrypt, gen_key_pair
from .signals import post_auth_success, post_auth_failed from .signals import post_auth_success, post_auth_failed
from .const import RSA_PRIVATE_KEY from .const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY
logger = get_logger(__name__) logger = get_logger(__name__)
@ -79,7 +80,70 @@ def authenticate(request=None, **credentials):
auth.authenticate = authenticate auth.authenticate = authenticate
class AuthMixin: class PasswordEncryptionViewMixin:
request = None
def get_decrypted_password(self, password=None, username=None):
request = self.request
if hasattr(request, 'data'):
data = request.data
else:
data = request.POST
username = username or data.get('username')
password = password or data.get('password')
password = self.decrypt_passwd(password)
if not password:
self.raise_password_decrypt_failed(username=username)
return password
def raise_password_decrypt_failed(self, username):
ip = self.get_request_ip()
raise errors.CredentialError(
error=errors.reason_password_decrypt_failed,
username=username, ip=ip, request=self.request
)
def decrypt_passwd(self, raw_passwd):
# 获取解密密钥,对密码进行解密
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
if rsa_private_key is not None:
try:
return rsa_decrypt(raw_passwd, rsa_private_key)
except Exception as e:
logger.error(e, exc_info=True)
logger.error(
f'Decrypt password failed: password[{raw_passwd}] '
f'rsa_private_key[{rsa_private_key}]'
)
return None
return raw_passwd
def get_request_ip(self):
ip = ''
if hasattr(self.request, 'data'):
ip = self.request.data.get('remote_addr', '')
ip = ip or get_request_ip(self.request)
return ip
def get_context_data(self, **kwargs):
# 生成加解密密钥对public_key传递给前端private_key存入session中供解密使用
rsa_public_key = self.request.session.get(RSA_PUBLIC_KEY)
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
if not all((rsa_private_key, rsa_public_key)):
rsa_private_key, rsa_public_key = gen_key_pair()
rsa_public_key = rsa_public_key.replace('\n', '\\n')
self.request.session[RSA_PRIVATE_KEY] = rsa_private_key
self.request.session[RSA_PUBLIC_KEY] = rsa_public_key
kwargs.update({
'rsa_public_key': rsa_public_key,
})
return super().get_context_data(**kwargs)
class AuthMixin(PasswordEncryptionViewMixin):
request = None request = None
partial_credential_error = None partial_credential_error = None
@ -106,13 +170,6 @@ class AuthMixin:
user.backend = self.request.session.get("auth_backend") user.backend = self.request.session.get("auth_backend")
return user return user
def get_request_ip(self):
ip = ''
if hasattr(self.request, 'data'):
ip = self.request.data.get('remote_addr', '')
ip = ip or get_request_ip(self.request)
return ip
def _check_is_block(self, username, raise_exception=True): def _check_is_block(self, username, raise_exception=True):
ip = self.get_request_ip() ip = self.get_request_ip()
if LoginBlockUtil(username, ip).is_block(): if LoginBlockUtil(username, ip).is_block():
@ -130,19 +187,6 @@ class AuthMixin:
username = self.request.POST.get("username") username = self.request.POST.get("username")
self._check_is_block(username, raise_exception) self._check_is_block(username, raise_exception)
def decrypt_passwd(self, raw_passwd):
# 获取解密密钥,对密码进行解密
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
if rsa_private_key is not None:
try:
return rsa_decrypt(raw_passwd, rsa_private_key)
except Exception as e:
logger.error(e, exc_info=True)
logger.error(f'Decrypt password failed: password[{raw_passwd}] '
f'rsa_private_key[{rsa_private_key}]')
return None
return raw_passwd
def raise_credential_error(self, error): def raise_credential_error(self, error):
raise self.partial_credential_error(error=error) raise self.partial_credential_error(error=error)
@ -158,14 +202,12 @@ class AuthMixin:
items = ['username', 'password', 'challenge', 'public_key', 'auto_login'] items = ['username', 'password', 'challenge', 'public_key', 'auto_login']
username, password, challenge, public_key, auto_login = bulk_get(data, *items, default='') username, password, challenge, public_key, auto_login = bulk_get(data, *items, default='')
password = password + challenge.strip()
ip = self.get_request_ip() ip = self.get_request_ip()
self._set_partial_credential_error(username=username, ip=ip, request=request) self._set_partial_credential_error(username=username, ip=ip, request=request)
password = password + challenge.strip()
if decrypt_passwd: if decrypt_passwd:
password = self.decrypt_passwd(password) password = self.get_decrypted_password()
if not password:
self.raise_credential_error(errors.reason_password_decrypt_failed)
return username, password, public_key, ip, auto_login return username, password, public_key, ip, auto_login
def _check_only_allow_exists_user_auth(self, username): def _check_only_allow_exists_user_auth(self, username):
@ -309,12 +351,13 @@ class AuthMixin:
unset, url = user.mfa_enabled_but_not_set() unset, url = user.mfa_enabled_but_not_set()
if unset: if unset:
raise errors.MFAUnsetError(user, self.request, url) raise errors.MFAUnsetError(user, self.request, url)
raise errors.MFARequiredError() raise errors.MFARequiredError(mfa_types=user.get_supported_mfa_types())
def mark_mfa_ok(self): def mark_mfa_ok(self, mfa_type=MFAType.OTP):
self.request.session['auth_mfa'] = 1 self.request.session['auth_mfa'] = 1
self.request.session['auth_mfa_time'] = time.time() self.request.session['auth_mfa_time'] = time.time()
self.request.session['auth_mfa_type'] = 'otp' self.request.session['auth_mfa_required'] = ''
self.request.session['auth_mfa_type'] = mfa_type
def check_mfa_is_block(self, username, ip, raise_exception=True): def check_mfa_is_block(self, username, ip, raise_exception=True):
if MFABlockUtils(username, ip).is_block(): if MFABlockUtils(username, ip).is_block():
@ -325,11 +368,11 @@ class AuthMixin:
else: else:
return exception return exception
def check_user_mfa(self, code): def check_user_mfa(self, code, mfa_type=MFAType.OTP):
user = self.get_user_from_session() user = self.get_user_from_session()
ip = self.get_request_ip() ip = self.get_request_ip()
self.check_mfa_is_block(user.username, ip) self.check_mfa_is_block(user.username, ip)
ok = user.check_mfa(code) ok = user.check_mfa(code, mfa_type=mfa_type)
if ok: if ok:
self.mark_mfa_ok() self.mark_mfa_ok()
return return
@ -337,7 +380,7 @@ class AuthMixin:
raise errors.MFAFailedError( raise errors.MFAFailedError(
username=user.username, username=user.username,
request=self.request, request=self.request,
ip=ip ip=ip, mfa_type=mfa_type,
) )
def get_ticket(self): def get_ticket(self):
@ -363,14 +406,14 @@ class AuthMixin:
raise errors.LoginConfirmOtherError('', "Not found") raise errors.LoginConfirmOtherError('', "Not found")
if ticket.status_open: if ticket.status_open:
raise errors.LoginConfirmWaitError(ticket.id) raise errors.LoginConfirmWaitError(ticket.id)
elif ticket.action_approve: elif ticket.state_approve:
self.request.session["auth_confirm"] = "1" self.request.session["auth_confirm"] = "1"
return return
elif ticket.action_reject: elif ticket.state_reject:
raise errors.LoginConfirmOtherError( raise errors.LoginConfirmOtherError(
ticket.id, ticket.get_action_display() ticket.id, ticket.get_action_display()
) )
elif ticket.action_close: elif ticket.state_close:
raise errors.LoginConfirmOtherError( raise errors.LoginConfirmOtherError(
ticket.id, ticket.get_action_display() ticket.id, ticket.get_action_display()
) )
@ -391,7 +434,6 @@ class AuthMixin:
def clear_auth_mark(self): def clear_auth_mark(self):
self.request.session['auth_password'] = '' self.request.session['auth_password'] = ''
self.request.session['auth_user_id'] = '' self.request.session['auth_user_id'] = ''
self.request.session['auth_mfa'] = ''
self.request.session['auth_confirm'] = '' self.request.session['auth_confirm'] = ''
self.request.session['auth_ticket_id'] = '' self.request.session['auth_ticket_id'] = ''

View File

@ -71,15 +71,14 @@ class LoginConfirmSetting(CommonModelMixin):
from orgs.models import Organization from orgs.models import Organization
ticket_title = _('Login confirm') + ' {}'.format(self.user) ticket_title = _('Login confirm') + ' {}'.format(self.user)
ticket_meta = self.construct_confirm_ticket_meta(request) ticket_meta = self.construct_confirm_ticket_meta(request)
ticket_assignees = self.reviewers.all()
data = { data = {
'title': ticket_title, 'title': ticket_title,
'type': const.TicketTypeChoices.login_confirm.value, 'type': const.TicketType.login_confirm.value,
'meta': ticket_meta, 'meta': ticket_meta,
'org_id': Organization.ROOT_ID, 'org_id': Organization.ROOT_ID,
} }
ticket = Ticket.objects.create(**data) ticket = Ticket.objects.create(**data)
ticket.assignees.set(ticket_assignees) ticket.create_process_map_and_node(self.reviewers.all())
ticket.open(self.user) ticket.open(self.user)
return ticket return ticket

View File

@ -16,8 +16,8 @@ from .models import AccessKey, LoginConfirmSetting
__all__ = [ __all__ = [
'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer', 'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer',
'MFAChallengeSerializer', 'LoginConfirmSettingSerializer', 'SSOTokenSerializer', 'MFAChallengeSerializer', 'LoginConfirmSettingSerializer', 'SSOTokenSerializer',
'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer', 'RDPFileSerializer', 'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer',
'PasswordVerifySerializer', 'PasswordVerifySerializer', 'MFASelectTypeSerializer',
] ]
@ -77,6 +77,10 @@ class BearerTokenSerializer(serializers.Serializer):
return instance return instance
class MFASelectTypeSerializer(serializers.Serializer):
type = serializers.CharField()
class MFAChallengeSerializer(serializers.Serializer): class MFAChallengeSerializer(serializers.Serializer):
type = serializers.CharField(write_only=True, required=False, allow_blank=True) type = serializers.CharField(write_only=True, required=False, allow_blank=True)
code = serializers.CharField(write_only=True) code = serializers.CharField(write_only=True)
@ -166,7 +170,7 @@ class ConnectionTokenAssetSerializer(serializers.ModelSerializer):
class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer): class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = SystemUser model = SystemUser
fields = ['id', 'name', 'username', 'password', 'private_key'] fields = ['id', 'name', 'username', 'password', 'private_key', 'ad_domain']
class ConnectionTokenGatewaySerializer(serializers.ModelSerializer): class ConnectionTokenGatewaySerializer(serializers.ModelSerializer):

View File

@ -13,6 +13,10 @@ from .signals import post_auth_success, post_auth_failed
@receiver(user_logged_in) @receiver(user_logged_in)
def on_user_auth_login_success(sender, user, request, **kwargs): def on_user_auth_login_success(sender, user, request, **kwargs):
# 开启了 MFA且没有校验过
if user.mfa_enabled and not request.session.get('auth_mfa'):
request.session['auth_mfa_required'] = 1
if settings.USER_LOGIN_SINGLE_MACHINE_ENABLED: if settings.USER_LOGIN_SINGLE_MACHINE_ENABLED:
user_id = 'single_machine_login_' + str(user.id) user_id = 'single_machine_login_' + str(user.id)
session_key = cache.get(user_id) session_key = cache.get(user_id)

View File

@ -0,0 +1,97 @@
import random
from django.conf import settings
from django.core.cache import cache
from django.utils.translation import gettext_lazy as _
from common.message.backends.sms.alibaba import AlibabaSMS
from common.message.backends.sms import SMS
from common.utils import get_logger
from common.exceptions import JMSException
logger = get_logger(__file__)
class CodeExpired(JMSException):
default_code = 'verify_code_expired'
default_detail = _('The verification code has expired. Please resend it')
class CodeError(JMSException):
default_code = 'verify_code_error'
default_detail = _('The verification code is incorrect')
class CodeSendTooFrequently(JMSException):
default_code = 'code_send_too_frequently'
default_detail = _('Please wait {} seconds before sending')
def __init__(self, ttl):
super().__init__(detail=self.default_detail.format(ttl))
class VerifyCodeUtil:
KEY_TMPL = 'auth-verify_code-{}'
TIMEOUT = 60
def __init__(self, account, key_suffix=None, timeout=None):
self.account = account
self.key_suffix = key_suffix
self.code = ''
if key_suffix is not None:
self.key = self.KEY_TMPL.format(key_suffix)
else:
self.key = self.KEY_TMPL.format(account)
self.timeout = self.TIMEOUT if timeout is None else timeout
def touch(self):
"""
生成保存发送
"""
ttl = self.ttl()
if ttl > 0:
raise CodeSendTooFrequently(ttl)
self.generate()
self.save()
self.send()
def generate(self):
code = ''.join(random.sample('0123456789', 4))
self.code = code
return code
def clear(self):
cache.delete(self.key)
def save(self):
cache.set(self.key, self.code, self.timeout)
def send(self):
"""
发送信息的方法如果有错误直接抛出 api 异常
"""
account = self.account
code = self.code
sms = SMS()
sms.send_verify_code(account, code)
logger.info(f'Send sms verify code: account={account} code={code}')
def verify(self, code):
right = cache.get(self.key)
if not right:
raise CodeExpired
if right != code:
raise CodeError
self.clear()
return True
def ttl(self):
return cache.ttl(self.key)
def get_code(self):
return cache.get(self.key)

View File

@ -9,24 +9,60 @@
{% block content %} {% block content %}
<form class="m-t" role="form" method="post" action=""> <form class="m-t" role="form" method="post" action="">
{% csrf_token %} {% csrf_token %}
{% if 'otp_code' in form.errors %} {% if 'code' in form.errors %}
<p class="red-fonts">{{ form.otp_code.errors.as_text }}</p> <p class="red-fonts">{{ form.code.errors.as_text }}</p>
{% endif %} {% endif %}
<div class="form-group"> <div class="form-group">
<select class="form-control"> <select id="verify-method-select" name="mfa_type" class="form-control" onchange="select_change(this.value)">
<option value="otp" selected>{% trans 'One-time password' %}</option> {% for method in methods %}
<option value="{{ method.name }}" {% if method.selected %} selected {% endif %} {% if not method.enable %} disabled {% endif %}>{{ method.label }}</option>
{% endfor %}
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="text" class="form-control" name="otp_code" placeholder="" required="" autofocus="autofocus"> <input type="text" class="form-control" name="code" placeholder="" required="" autofocus="autofocus">
<span class="help-block"> <span class="help-block">
{% trans 'Open MFA Authenticator and enter the 6-bit dynamic code' %} {% trans 'Please enter the verification code' %}
</span> </span>
</div> </div>
<button id='send-sms-verify-code' class="btn btn-primary full-width m-b" onclick="sendSMSVerifyCode()" style="display: none">{% trans 'Send verification code' %}</button>
<button type="submit" class="btn btn-primary block full-width m-b">{% trans 'Next' %}</button> <button type="submit" class="btn btn-primary block full-width m-b">{% trans 'Next' %}</button>
<div> <div>
<small>{% trans "Can't provide security? Please contact the administrator!" %}</small> <small>{% trans "Can't provide security? Please contact the administrator!" %}</small>
</div> </div>
</form> </form>
<script>
var methodSelect = document.getElementById('verify-method-select');
if (methodSelect.value !== null) {
select_change(methodSelect.value);
}
function select_change(type){
var currentBtn = document.getElementById('send-sms-verify-code');
if (type == "sms") {
currentBtn.style.display = "block";
}
else {
currentBtn.style.display = "none";
}
}
function sendSMSVerifyCode(){
var url = "{% url 'api-auth:sms-verify-code-send' %}";
requestApi({
url: url,
method: "POST",
success: function (data) {
alert('验证码已发送');
},
error: function (text, data) {
alert(data.detail)
},
flash_message: false
})
}
</script>
{% endblock %} {% endblock %}

View File

@ -27,7 +27,9 @@ urlpatterns = [
path('auth/', api.TokenCreateApi.as_view(), name='user-auth'), path('auth/', api.TokenCreateApi.as_view(), name='user-auth'),
path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'), path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'),
path('mfa/challenge/', api.MFAChallengeApi.as_view(), name='mfa-challenge'), path('mfa/challenge/', api.MFAChallengeApi.as_view(), name='mfa-challenge'),
path('mfa/select/', api.MFASelectTypeApi.as_view(), name='mfa-select'),
path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'), path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'),
path('sms/verify-code/send/', api.SendSMSVerifyCodeApi.as_view(), name='sms-verify-code-send'),
path('password/verify/', api.UserPasswordVerifyApi.as_view(), name='user-password-verify'), path('password/verify/', api.UserPasswordVerifyApi.as_view(), name='user-password-verify'),
path('login-confirm-ticket/status/', api.TicketStatusApi.as_view(), name='login-confirm-ticket-status'), path('login-confirm-ticket/status/', api.TicketStatusApi.as_view(), name='login-confirm-ticket-status'),
path('login-confirm-settings/<uuid:user_id>/', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update') path('login-confirm-settings/<uuid:user_id>/', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update')

View File

@ -4,7 +4,6 @@ import base64
from Cryptodome.PublicKey import RSA from Cryptodome.PublicKey import RSA
from Cryptodome.Cipher import PKCS1_v1_5 from Cryptodome.Cipher import PKCS1_v1_5
from Cryptodome import Random from Cryptodome import Random
from common.utils import get_logger from common.utils import get_logger
logger = get_logger(__file__) logger = get_logger(__file__)

View File

@ -51,7 +51,8 @@ class UserLoginView(mixins.AuthMixin, FormView):
if settings.AUTH_OPENID: if settings.AUTH_OPENID:
auth_type = 'OIDC' auth_type = 'OIDC'
openid_auth_url = reverse(settings.AUTH_OPENID_AUTH_LOGIN_URL_NAME) + f'?next={next_url}' openid_auth_url = reverse(settings.AUTH_OPENID_AUTH_LOGIN_URL_NAME)
openid_auth_url = openid_auth_url + f'?next={next_url}'
else: else:
openid_auth_url = None openid_auth_url = None
@ -64,16 +65,13 @@ class UserLoginView(mixins.AuthMixin, FormView):
if not any([openid_auth_url, cas_auth_url]): if not any([openid_auth_url, cas_auth_url]):
return None return None
if settings.LOGIN_REDIRECT_TO_BACKEND == 'OPENID' and openid_auth_url: login_redirect = settings.LOGIN_REDIRECT_TO_BACKEND.lower()
if login_redirect == ['CAS', 'cas'] and cas_auth_url:
auth_url = cas_auth_url
else:
auth_url = openid_auth_url auth_url = openid_auth_url
elif settings.LOGIN_REDIRECT_TO_BACKEND == 'CAS' and cas_auth_url: if settings.LOGIN_REDIRECT_TO_BACKEND or not settings.LOGIN_REDIRECT_MSG_ENABLED:
auth_url = cas_auth_url
else:
auth_url = openid_auth_url or cas_auth_url
if settings.LOGIN_REDIRECT_TO_BACKEND:
redirect_url = auth_url redirect_url = auth_url
else: else:
message_data = { message_data = {
@ -137,15 +135,6 @@ class UserLoginView(mixins.AuthMixin, FormView):
self.request.session[RSA_PUBLIC_KEY] = None self.request.session[RSA_PUBLIC_KEY] = None
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# 生成加解密密钥对public_key传递给前端private_key存入session中供解密使用
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
rsa_public_key = self.request.session.get(RSA_PUBLIC_KEY)
if not all((rsa_private_key, rsa_public_key)):
rsa_private_key, rsa_public_key = utils.gen_key_pair()
rsa_public_key = rsa_public_key.replace('\n', '\\n')
self.request.session[RSA_PRIVATE_KEY] = rsa_private_key
self.request.session[RSA_PUBLIC_KEY] = rsa_public_key
forgot_password_url = reverse('authentication:forgot-password') forgot_password_url = reverse('authentication:forgot-password')
has_other_auth_backend = settings.AUTHENTICATION_BACKENDS[0] != settings.AUTH_BACKEND_MODEL has_other_auth_backend = settings.AUTHENTICATION_BACKENDS[0] != settings.AUTH_BACKEND_MODEL
if has_other_auth_backend and settings.FORGOT_PASSWORD_URL: if has_other_auth_backend and settings.FORGOT_PASSWORD_URL:
@ -158,7 +147,6 @@ class UserLoginView(mixins.AuthMixin, FormView):
'AUTH_WECOM': settings.AUTH_WECOM, 'AUTH_WECOM': settings.AUTH_WECOM,
'AUTH_DINGTALK': settings.AUTH_DINGTALK, 'AUTH_DINGTALK': settings.AUTH_DINGTALK,
'AUTH_FEISHU': settings.AUTH_FEISHU, 'AUTH_FEISHU': settings.AUTH_FEISHU,
'rsa_public_key': rsa_public_key,
'forgot_password_url': forgot_password_url 'forgot_password_url': forgot_password_url
} }
kwargs.update(context) kwargs.update(context)

View File

@ -3,6 +3,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from .. import forms, errors, mixins from .. import forms, errors, mixins
from .utils import redirect_to_guard_view from .utils import redirect_to_guard_view
@ -18,12 +20,14 @@ class UserLoginOtpView(mixins.AuthMixin, FormView):
redirect_field_name = 'next' redirect_field_name = 'next'
def form_valid(self, form): def form_valid(self, form):
otp_code = form.cleaned_data.get('otp_code') otp_code = form.cleaned_data.get('code')
mfa_type = form.cleaned_data.get('mfa_type')
try: try:
self.check_user_mfa(otp_code) self.check_user_mfa(otp_code, mfa_type)
return redirect_to_guard_view() return redirect_to_guard_view()
except (errors.MFAFailedError, errors.BlockMFAError) as e: except (errors.MFAFailedError, errors.BlockMFAError) as e:
form.add_error('otp_code', e.msg) form.add_error('code', e.msg)
return super().form_invalid(form) return super().form_invalid(form)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
@ -31,3 +35,28 @@ class UserLoginOtpView(mixins.AuthMixin, FormView):
traceback.print_exception() traceback.print_exception()
return redirect_to_guard_view() return redirect_to_guard_view()
def get_context_data(self, **kwargs):
user = self.get_user_from_session()
context = {
'methods': [
{
'name': 'otp',
'label': _('One-time password'),
'enable': bool(user.otp_secret_key),
'selected': False,
},
{
'name': 'sms',
'label': _('SMS'),
'enable': bool(user.phone) and settings.SMS_ENABLED and settings.XPACK_ENABLED,
'selected': False,
},
]
}
for item in context['methods']:
if item['enable']:
item['selected'] = True
break
context.update(kwargs)
return context

20
apps/common/db/encoder.py Normal file
View File

@ -0,0 +1,20 @@
import json
from datetime import datetime
import uuid
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
class ModelJSONFieldEncoder(json.JSONEncoder):
""" 解决一些类型的字段不能序列化的问题 """
def default(self, obj):
if isinstance(obj, datetime):
return obj.strftime(settings.DATETIME_DISPLAY_FORMAT)
if isinstance(obj, uuid.UUID):
return str(obj)
if isinstance(obj, type(_("ugettext_lazy"))):
return str(obj)
else:
return super().default(obj)

View File

@ -160,7 +160,7 @@ class BaseService(object):
if self.process: if self.process:
try: try:
self.process.wait(1) # 不wait子进程可能无法回收 self.process.wait(1) # 不wait子进程可能无法回收
except subprocess.TimeoutExpired: except:
pass pass
if self.is_running: if self.is_running:

View File

@ -2,9 +2,12 @@ import time
import hmac import hmac
import base64 import base64
from common.utils import get_logger
from common.message.backends.utils import digest, as_request from common.message.backends.utils import digest, as_request
from common.message.backends.mixin import BaseRequest from common.message.backends.mixin import BaseRequest
logger = get_logger(__file__)
def sign(secret, data): def sign(secret, data):
@ -160,6 +163,7 @@ class DingTalk:
} }
} }
} }
logger.info(f'Dingtalk send text: user_ids={user_ids} msg={msg}')
data = self._request.post(URL.SEND_MESSAGE, json=body, with_token=True) data = self._request.post(URL.SEND_MESSAGE, json=body, with_token=True)
return data return data

View File

@ -106,6 +106,7 @@ class FeiShu(RequestMixin):
body['receive_id'] = user_id body['receive_id'] = user_id
try: try:
logger.info(f'Feishu send text: user_ids={user_ids} msg={msg}')
self._requests.post(URL.SEND_MESSAGE, params=params, json=body) self._requests.post(URL.SEND_MESSAGE, params=params, json=body)
except APIException as e: except APIException as e:
# 只处理可预知的错误 # 只处理可预知的错误

View File

@ -0,0 +1,84 @@
from collections import OrderedDict
import importlib
from django.utils.translation import gettext_lazy as _
from django.db.models import TextChoices
from django.conf import settings
from common.utils import get_logger
from common.exceptions import JMSException
logger = get_logger(__file__)
class SMS_MESSAGE(TextChoices):
"""
定义短信的各种消息类型会存到类似 `ALIBABA_SMS_SIGN_AND_TEMPLATES` settings
{
'verification_code': {'sign_name': 'Jumpserver', 'template_code': 'SMS_222870834'}
...
}
"""
"""
验证码签名和模板模板例子:
`您的验证码${code}您正进行身份验证打死不告诉别人`
其中必须包含 `code` 变量
"""
VERIFICATION_CODE = 'verification_code'
def get_sign_and_tmpl(self, config: dict):
try:
data = config[self]
return data['sign_name'], data['template_code']
except KeyError as e:
raise JMSException(
code=f'{settings.SMS_BACKEND}_sign_and_tmpl_bad',
detail=_('Invalid SMS sign and template: {}').format(e)
)
class BACKENDS(TextChoices):
ALIBABA = 'alibaba', _('Alibaba')
TENCENT = 'tencent', _('Tencent')
class BaseSMSClient:
"""
短信终端的基类
"""
SIGN_AND_TMPL_SETTING_FIELD: str
@property
def sign_and_tmpl(self):
return getattr(settings, self.SIGN_AND_TMPL_SETTING_FIELD, {})
@classmethod
def new_from_settings(cls):
raise NotImplementedError
def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
raise NotImplementedError
class SMS:
client: BaseSMSClient
def __init__(self, backend=None):
m = importlib.import_module(f'.{backend or settings.SMS_BACKEND}', __package__)
self.client = m.client.new_from_settings()
def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
return self.client.send_sms(
phone_numbers=phone_numbers,
sign_name=sign_name,
template_code=template_code,
template_param=template_param,
**kwargs
)
def send_verify_code(self, phone_number, code):
sign_name, template_code = SMS_MESSAGE.VERIFICATION_CODE.get_sign_and_tmpl(self.client.sign_and_tmpl)
return self.send_sms([phone_number], sign_name, template_code, OrderedDict(code=code))

View File

@ -0,0 +1,61 @@
import json
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from alibabacloud_dysmsapi20170525.client import Client as Dysmsapi20170525Client
from alibabacloud_tea_openapi import models as open_api_models
from alibabacloud_dysmsapi20170525 import models as dysmsapi_20170525_models
from Tea.exceptions import TeaException
from common.utils import get_logger
from common.exceptions import JMSException
from . import BaseSMSClient
logger = get_logger(__file__)
class AlibabaSMS(BaseSMSClient):
SIGN_AND_TMPL_SETTING_FIELD = 'ALIBABA_SMS_SIGN_AND_TEMPLATES'
@classmethod
def new_from_settings(cls):
return cls(
access_key_id=settings.ALIBABA_ACCESS_KEY_ID,
access_key_secret=settings.ALIBABA_ACCESS_KEY_SECRET
)
def __init__(self, access_key_id: str, access_key_secret: str):
config = open_api_models.Config(
# 您的AccessKey ID,
access_key_id=access_key_id,
# 您的AccessKey Secret,
access_key_secret=access_key_secret
)
# 访问的域名
config.endpoint = 'dysmsapi.aliyuncs.com'
self.client = Dysmsapi20170525Client(config)
def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
phone_numbers_str = ','.join(phone_numbers)
send_sms_request = dysmsapi_20170525_models.SendSmsRequest(
phone_numbers=phone_numbers_str, sign_name=sign_name,
template_code=template_code, template_param=json.dumps(template_param)
)
try:
logger.info(f'Alibaba sms send: '
f'phone_numbers={phone_numbers} '
f'sign_name={sign_name} '
f'template_code={template_code} '
f'template_param={template_param}')
response = self.client.send_sms(send_sms_request)
# 这里只判断是否成功,失败抛出异常
if response.body.code != 'OK':
raise JMSException(detail=response.body.message, code=response.body.code)
except TeaException as e:
if e.code == 'SignatureDoesNotMatch':
raise JMSException(code=e.code, detail=_('Signature does not match'))
raise JMSException(code=e.code, detail=e.message)
return response
client = AlibabaSMS

View File

@ -0,0 +1,90 @@
import json
from collections import OrderedDict
from django.conf import settings
from common.exceptions import JMSException
from common.utils import get_logger
from tencentcloud.common import credential
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
# 导入对应产品模块的client models。
from tencentcloud.sms.v20210111 import sms_client, models
# 导入可选配置类
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from . import BaseSMSClient
logger = get_logger(__file__)
class TencentSMS(BaseSMSClient):
"""
https://cloud.tencent.com/document/product/382/43196#.E5.8F.91.E9.80.81.E7.9F.AD.E4.BF.A1
"""
SIGN_AND_TMPL_SETTING_FIELD = 'TENCENT_SMS_SIGN_AND_TEMPLATES'
@classmethod
def new_from_settings(cls):
return cls(
secret_id=settings.TENCENT_SECRET_ID,
secret_key=settings.TENCENT_SECRET_KEY,
sdkappid=settings.TENCENT_SDKAPPID
)
def __init__(self, secret_id: str, secret_key: str, sdkappid: str):
self.sdkappid = sdkappid
cred = credential.Credential(secret_id, secret_key)
httpProfile = HttpProfile()
httpProfile.reqMethod = "POST" # post请求(默认为post请求)
httpProfile.reqTimeout = 30 # 请求超时时间,单位为秒(默认60秒)
httpProfile.endpoint = "sms.tencentcloudapi.com"
clientProfile = ClientProfile()
clientProfile.signMethod = "TC3-HMAC-SHA256" # 指定签名算法
clientProfile.language = "en-US"
clientProfile.httpProfile = httpProfile
self.client = sms_client.SmsClient(cred, "ap-guangzhou", clientProfile)
def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: OrderedDict, **kwargs):
try:
req = models.SendSmsRequest()
# 基本类型的设置:
# SDK采用的是指针风格指定参数即使对于基本类型你也需要用指针来对参数赋值。
# SDK提供对基本类型的指针引用封装函数
# 帮助链接:
# 短信控制台: https://console.cloud.tencent.com/smsv2
# sms helper: https://cloud.tencent.com/document/product/382/3773
# 短信应用ID: 短信SdkAppId在 [短信控制台] 添加应用后生成的实际SdkAppId示例如1400006666
req.SmsSdkAppId = self.sdkappid
# 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名,签名信息可登录 [短信控制台] 查看
req.SignName = sign_name
# 短信码号扩展号: 默认未开通,如需开通请联系 [sms helper]
req.ExtendCode = ""
# 用户的 session 内容: 可以携带用户侧 ID 等上下文信息server 会原样返回
req.SessionContext = "Jumpserver"
# 国际/港澳台短信 senderid: 国内短信填空,默认未开通,如需开通请联系 [sms helper]
req.SenderId = ""
# 下发手机号码,采用 E.164 标准,+[国家或地区码][手机号]
# 示例如:+8613711112222 其中前面有一个+号 86为国家码13711112222为手机号最多不要超过200个手机号
req.PhoneNumberSet = phone_numbers
# 模板 ID: 必须填写已审核通过的模板 ID。模板ID可登录 [短信控制台] 查看
req.TemplateId = template_code
# 模板参数: 若无模板参数,则设置为空
req.TemplateParamSet = list(template_param.values())
# 通过client对象调用DescribeInstances方法发起请求。注意请求方法名与请求对象是对应的。
# 返回的resp是一个DescribeInstancesResponse类的实例与请求对象对应。
logger.info(f'Tencent sms send: '
f'phone_numbers={phone_numbers} '
f'sign_name={sign_name} '
f'template_code={template_code} '
f'template_param={template_param}')
resp = self.client.SendSms(req)
return resp
except TencentCloudSDKException as e:
raise JMSException(code=e.code, detail=e.message)
client = TencentSMS

View File

@ -115,6 +115,7 @@ class WeCom(RequestMixin):
}, },
**extra_params **extra_params
} }
logger.info(f'Wecom send text: users={users} msg={msg}')
data = self._requests.post(URL.SEND_MESSAGE, json=body, check_errcode_is_0=False) data = self._requests.post(URL.SEND_MESSAGE, json=body, check_errcode_is_0=False)
errcode = data['errcode'] errcode = data['errcode']

View File

@ -191,3 +191,11 @@ class HasQueryParamsUserAndIsCurrentOrgMember(permissions.BasePermission):
return False return False
query_user = current_org.get_members().filter(id=query_user_id).first() query_user = current_org.get_members().filter(id=query_user_id).first()
return bool(query_user) return bool(query_user)
class OnlySuperUserCanList(IsValidUser):
def has_permission(self, request, view):
user = request.user
if view.action == 'list' and not user.is_superuser:
return False
return True

View File

@ -179,13 +179,14 @@ class Config(dict):
'AUTH_OPENID_CLIENT_SECRET': 'client-secret', 'AUTH_OPENID_CLIENT_SECRET': 'client-secret',
'AUTH_OPENID_SHARE_SESSION': True, 'AUTH_OPENID_SHARE_SESSION': True,
'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True, 'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True,
# OpenID 新配置参数 (version >= 1.5.9) # OpenID 新配置参数 (version >= 1.5.9)
'AUTH_OPENID_PROVIDER_ENDPOINT': 'https://op-example.com/', 'AUTH_OPENID_PROVIDER_ENDPOINT': 'https://oidc.example.com/',
'AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT': 'https://op-example.com/authorize', 'AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT': 'https://oidc.example.com/authorize',
'AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT': 'https://op-example.com/token', 'AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT': 'https://oidc.example.com/token',
'AUTH_OPENID_PROVIDER_JWKS_ENDPOINT': 'https://op-example.com/jwks', 'AUTH_OPENID_PROVIDER_JWKS_ENDPOINT': 'https://oidc.example.com/jwks',
'AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT': 'https://op-example.com/userinfo', 'AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT': 'https://oidc.example.com/userinfo',
'AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT': 'https://op-example.com/logout', 'AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT': 'https://oidc.example.com/logout',
'AUTH_OPENID_PROVIDER_SIGNATURE_ALG': 'HS256', 'AUTH_OPENID_PROVIDER_SIGNATURE_ALG': 'HS256',
'AUTH_OPENID_PROVIDER_SIGNATURE_KEY': None, 'AUTH_OPENID_PROVIDER_SIGNATURE_KEY': None,
'AUTH_OPENID_SCOPES': 'openid profile email', 'AUTH_OPENID_SCOPES': 'openid profile email',
@ -194,10 +195,13 @@ class Config(dict):
'AUTH_OPENID_USE_STATE': True, 'AUTH_OPENID_USE_STATE': True,
'AUTH_OPENID_USE_NONCE': True, 'AUTH_OPENID_USE_NONCE': True,
'AUTH_OPENID_ALWAYS_UPDATE_USER': True, 'AUTH_OPENID_ALWAYS_UPDATE_USER': True,
# OpenID 旧配置参数 (version <= 1.5.8 (discarded))
'AUTH_OPENID_SERVER_URL': 'http://openid', # Keycloak 旧配置参数 (version <= 1.5.8 (discarded))
'AUTH_OPENID_KEYCLOAK': True,
'AUTH_OPENID_SERVER_URL': 'https://keycloak.example.com',
'AUTH_OPENID_REALM_NAME': None, 'AUTH_OPENID_REALM_NAME': None,
# Raidus 认证
'AUTH_RADIUS': False, 'AUTH_RADIUS': False,
'RADIUS_SERVER': 'localhost', 'RADIUS_SERVER': 'localhost',
'RADIUS_PORT': 1812, 'RADIUS_PORT': 1812,
@ -205,8 +209,9 @@ class Config(dict):
'RADIUS_ENCRYPT_PASSWORD': True, 'RADIUS_ENCRYPT_PASSWORD': True,
'OTP_IN_RADIUS': False, 'OTP_IN_RADIUS': False,
# Cas 认证
'AUTH_CAS': False, 'AUTH_CAS': False,
'CAS_SERVER_URL': "http://host/cas/", 'CAS_SERVER_URL': "https://example.com/cas/",
'CAS_ROOT_PROXIED_AS': '', 'CAS_ROOT_PROXIED_AS': '',
'CAS_LOGOUT_COMPLETELY': True, 'CAS_LOGOUT_COMPLETELY': True,
'CAS_VERSION': 3, 'CAS_VERSION': 3,
@ -218,24 +223,44 @@ class Config(dict):
'AUTH_SSO': False, 'AUTH_SSO': False,
'AUTH_SSO_AUTHKEY_TTL': 60 * 15, 'AUTH_SSO_AUTHKEY_TTL': 60 * 15,
# 企业微信
'AUTH_WECOM': False, 'AUTH_WECOM': False,
'WECOM_CORPID': '', 'WECOM_CORPID': '',
'WECOM_AGENTID': '', 'WECOM_AGENTID': '',
'WECOM_SECRET': '', 'WECOM_SECRET': '',
# 钉钉
'AUTH_DINGTALK': False, 'AUTH_DINGTALK': False,
'DINGTALK_AGENTID': '', 'DINGTALK_AGENTID': '',
'DINGTALK_APPKEY': '', 'DINGTALK_APPKEY': '',
'DINGTALK_APPSECRET': '', 'DINGTALK_APPSECRET': '',
# 飞书
'AUTH_FEISHU': False, 'AUTH_FEISHU': False,
'FEISHU_APP_ID': '', 'FEISHU_APP_ID': '',
'FEISHU_APP_SECRET': '', 'FEISHU_APP_SECRET': '',
'LOGIN_REDIRECT_TO_BACKEND': '', # 'OPENID / CAS
'LOGIN_REDIRECT_MSG_ENABLED': True,
'SMS_ENABLED': False,
'SMS_BACKEND': '',
'SMS_TEST_PHONE': '',
'ALIBABA_ACCESS_KEY_ID': '',
'ALIBABA_ACCESS_KEY_SECRET': '',
'ALIBABA_SMS_SIGN_AND_TEMPLATES': {},
'TENCENT_SECRET_ID': '',
'TENCENT_SECRET_KEY': '',
'TENCENT_SDKAPPID': '',
'TENCENT_SMS_SIGN_AND_TEMPLATES': {},
'OTP_VALID_WINDOW': 2, 'OTP_VALID_WINDOW': 2,
'OTP_ISSUER_NAME': 'JumpServer', 'OTP_ISSUER_NAME': 'JumpServer',
'EMAIL_SUFFIX': 'jumpserver.org', 'EMAIL_SUFFIX': 'example.com',
# Terminal配置
'TERMINAL_PASSWORD_AUTH': True, 'TERMINAL_PASSWORD_AUTH': True,
'TERMINAL_PUBLIC_KEY_AUTH': True, 'TERMINAL_PUBLIC_KEY_AUTH': True,
'TERMINAL_HEARTBEAT_INTERVAL': 20, 'TERMINAL_HEARTBEAT_INTERVAL': 20,
@ -245,7 +270,10 @@ class Config(dict):
'TERMINAL_HOST_KEY': '', 'TERMINAL_HOST_KEY': '',
'TERMINAL_TELNET_REGEX': '', 'TERMINAL_TELNET_REGEX': '',
'TERMINAL_COMMAND_STORAGE': {}, 'TERMINAL_COMMAND_STORAGE': {},
'TERMINAL_RDP_ADDR': '',
'XRDP_ENABLED': True,
# 安全配置
'SECURITY_MFA_AUTH': 0, # 0 不开启 1 全局开启 2 管理员开启 'SECURITY_MFA_AUTH': 0, # 0 不开启 1 全局开启 2 管理员开启
'SECURITY_COMMAND_EXECUTION': True, 'SECURITY_COMMAND_EXECUTION': True,
'SECURITY_SERVICE_ACCOUNT_REGISTRATION': True, 'SECURITY_SERVICE_ACCOUNT_REGISTRATION': True,
@ -262,57 +290,60 @@ class Config(dict):
'SECURITY_PASSWORD_SPECIAL_CHAR': False, 'SECURITY_PASSWORD_SPECIAL_CHAR': False,
'SECURITY_LOGIN_CHALLENGE_ENABLED': False, 'SECURITY_LOGIN_CHALLENGE_ENABLED': False,
'SECURITY_LOGIN_CAPTCHA_ENABLED': True, 'SECURITY_LOGIN_CAPTCHA_ENABLED': True,
'SECURITY_DATA_CRYPTO_ALGO': 'aes',
'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, 'SECURITY_LUNA_REMEMBER_AUTH': True,
'SECURITY_WATERMARK_ENABLED': True, 'SECURITY_WATERMARK_ENABLED': True,
'SECURITY_MFA_VERIFY_TTL': 3600,
'SECURITY_SESSION_SHARE': True,
'OLD_PASSWORD_HISTORY_LIMIT_COUNT': 5,
'LOGIN_CONFIRM_ENABLE': False, # 准备废弃,放到 acl 中
'CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED': True,
'USER_LOGIN_SINGLE_MACHINE_ENABLED': False,
'ONLY_ALLOW_EXIST_USER_AUTH': False,
'ONLY_ALLOW_AUTH_FROM_SOURCE': False,
# 启动前
'HTTP_BIND_HOST': '0.0.0.0', 'HTTP_BIND_HOST': '0.0.0.0',
'HTTP_LISTEN_PORT': 8080, 'HTTP_LISTEN_PORT': 8080,
'WS_LISTEN_PORT': 8070, 'WS_LISTEN_PORT': 8070,
'SYSLOG_ADDR': '', # '192.168.0.1:514'
'SYSLOG_FACILITY': 'user',
'SYSLOG_SOCKTYPE': 2,
'PERM_EXPIRED_CHECK_PERIODIC': 60 * 60,
'FLOWER_URL': "127.0.0.1:5555",
'LANGUAGE_CODE': 'zh',
'TIME_ZONE': 'Asia/Shanghai',
'FORCE_SCRIPT_NAME': '',
'SESSION_COOKIE_SECURE': False,
'CSRF_COOKIE_SECURE': False,
'REFERER_CHECK_ENABLED': False,
'SESSION_SAVE_EVERY_REQUEST': True,
'SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE': False,
'SERVER_REPLAY_STORAGE': {},
'SECURITY_DATA_CRYPTO_ALGO': 'aes',
# 记录清理清理
'LOGIN_LOG_KEEP_DAYS': 200, 'LOGIN_LOG_KEEP_DAYS': 200,
'TASK_LOG_KEEP_DAYS': 90, 'TASK_LOG_KEEP_DAYS': 90,
'OPERATE_LOG_KEEP_DAYS': 200, 'OPERATE_LOG_KEEP_DAYS': 200,
'FTP_LOG_KEEP_DAYS': 200, 'FTP_LOG_KEEP_DAYS': 200,
'ASSETS_PERM_CACHE_TIME': 3600 * 24,
'SECURITY_MFA_VERIFY_TTL': 3600,
'OLD_PASSWORD_HISTORY_LIMIT_COUNT': 5,
'ASSETS_PERM_CACHE_ENABLE': HAS_XPACK,
'SYSLOG_ADDR': '', # '192.168.0.1:514'
'SYSLOG_FACILITY': 'user',
'SYSLOG_SOCKTYPE': 2,
'PERM_SINGLE_ASSET_TO_UNGROUP_NODE': False,
'PERM_EXPIRED_CHECK_PERIODIC': 60 * 60,
'WINDOWS_SSH_DEFAULT_SHELL': 'cmd',
'FLOWER_URL': "127.0.0.1:5555",
'DEFAULT_ORG_SHOW_ALL_USERS': True,
'PERIOD_TASK_ENABLED': True,
'FORCE_SCRIPT_NAME': '',
'LOGIN_CONFIRM_ENABLE': False,
'WINDOWS_SKIP_ALL_MANUAL_PASSWORD': False,
'ORG_CHANGE_TO_URL': '',
'LANGUAGE_CODE': 'zh',
'TIME_ZONE': 'Asia/Shanghai',
'CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED': True,
'USER_LOGIN_SINGLE_MACHINE_ENABLED': False,
'TICKETS_ENABLED': True,
'SESSION_COOKIE_SECURE': False,
'CSRF_COOKIE_SECURE': False,
'REFERER_CHECK_ENABLED': False,
'SERVER_REPLAY_STORAGE': {},
'CONNECTION_TOKEN_ENABLED': False,
'ONLY_ALLOW_EXIST_USER_AUTH': False,
'ONLY_ALLOW_AUTH_FROM_SOURCE': False,
'SESSION_SAVE_EVERY_REQUEST': True,
'SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE': False,
'FORGOT_PASSWORD_URL': '',
'HEALTH_CHECK_TOKEN': '',
'LOGIN_REDIRECT_TO_BACKEND': None, # 'OPENID / CAS
'CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS': 30, 'CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS': 30,
'TERMINAL_RDP_ADDR': '' # 废弃的
'DEFAULT_ORG_SHOW_ALL_USERS': True,
'ORG_CHANGE_TO_URL': '',
'WINDOWS_SKIP_ALL_MANUAL_PASSWORD': False,
'CONNECTION_TOKEN_ENABLED': False,
'PERM_SINGLE_ASSET_TO_UNGROUP_NODE': False,
'WINDOWS_SSH_DEFAULT_SHELL': 'cmd',
'PERIOD_TASK_ENABLED': True,
'TICKETS_ENABLED': True,
'FORGOT_PASSWORD_URL': '',
'HEALTH_CHECK_TOKEN': '',
} }
def compatible_auth_openid_of_key(self): def compatible_auth_openid_of_key(self):
@ -323,6 +354,9 @@ class Config(dict):
构造出新配置中标准OpenID协议中所需的Endpoint即可 构造出新配置中标准OpenID协议中所需的Endpoint即可
(Keycloak说明文档参考: https://www.keycloak.org/docs/latest/securing_apps/) (Keycloak说明文档参考: https://www.keycloak.org/docs/latest/securing_apps/)
""" """
if self.AUTH_OPENID and not self.AUTH_OPENID_REALM_NAME:
self['AUTH_OPENID_KEYCLOAK'] = False
if not self.AUTH_OPENID: if not self.AUTH_OPENID:
return return

View File

@ -122,6 +122,22 @@ AUTH_FEISHU = CONFIG.AUTH_FEISHU
FEISHU_APP_ID = CONFIG.FEISHU_APP_ID FEISHU_APP_ID = CONFIG.FEISHU_APP_ID
FEISHU_APP_SECRET = CONFIG.FEISHU_APP_SECRET FEISHU_APP_SECRET = CONFIG.FEISHU_APP_SECRET
# SMS auth
SMS_ENABLED = CONFIG.SMS_ENABLED
SMS_BACKEND = CONFIG.SMS_BACKEND
SMS_TEST_PHONE = CONFIG.SMS_TEST_PHONE
# Alibaba
ALIBABA_ACCESS_KEY_ID = CONFIG.ALIBABA_ACCESS_KEY_ID
ALIBABA_ACCESS_KEY_SECRET = CONFIG.ALIBABA_ACCESS_KEY_SECRET
ALIBABA_SMS_SIGN_AND_TEMPLATES = CONFIG.ALIBABA_SMS_SIGN_AND_TEMPLATES
# TENCENT
TENCENT_SECRET_ID = CONFIG.TENCENT_SECRET_ID
TENCENT_SECRET_KEY = CONFIG.TENCENT_SECRET_KEY
TENCENT_SDKAPPID = CONFIG.TENCENT_SDKAPPID
TENCENT_SMS_SIGN_AND_TEMPLATES = CONFIG.TENCENT_SMS_SIGN_AND_TEMPLATES
# Other setting # Other setting
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
LOGIN_CONFIRM_ENABLE = CONFIG.LOGIN_CONFIRM_ENABLE LOGIN_CONFIRM_ENABLE = CONFIG.LOGIN_CONFIRM_ENABLE

View File

@ -87,6 +87,7 @@ MIDDLEWARE = [
'orgs.middleware.OrgMiddleware', 'orgs.middleware.OrgMiddleware',
'authentication.backends.oidc.middleware.OIDCRefreshIDTokenMiddleware', 'authentication.backends.oidc.middleware.OIDCRefreshIDTokenMiddleware',
'authentication.backends.cas.middleware.CASMiddleware', 'authentication.backends.cas.middleware.CASMiddleware',
'authentication.middleware.MFAMiddleware',
'simple_history.middleware.HistoryRequestMiddleware', 'simple_history.middleware.HistoryRequestMiddleware',
] ]

View File

@ -72,14 +72,9 @@ TERMINAL_HOST_KEY = CONFIG.TERMINAL_HOST_KEY
TERMINAL_HEADER_TITLE = CONFIG.TERMINAL_HEADER_TITLE TERMINAL_HEADER_TITLE = CONFIG.TERMINAL_HEADER_TITLE
TERMINAL_TELNET_REGEX = CONFIG.TERMINAL_TELNET_REGEX TERMINAL_TELNET_REGEX = CONFIG.TERMINAL_TELNET_REGEX
# User or user group permission cache time, default 3600 seconds
ASSETS_PERM_CACHE_ENABLE = CONFIG.ASSETS_PERM_CACHE_ENABLE
ASSETS_PERM_CACHE_TIME = CONFIG.ASSETS_PERM_CACHE_TIME
# Asset user auth external backend, default AuthBook backend # Asset user auth external backend, default AuthBook backend
BACKEND_ASSET_USER_AUTH_VAULT = False BACKEND_ASSET_USER_AUTH_VAULT = False
DEFAULT_ORG_SHOW_ALL_USERS = CONFIG.DEFAULT_ORG_SHOW_ALL_USERS
PERM_SINGLE_ASSET_TO_UNGROUP_NODE = CONFIG.PERM_SINGLE_ASSET_TO_UNGROUP_NODE PERM_SINGLE_ASSET_TO_UNGROUP_NODE = CONFIG.PERM_SINGLE_ASSET_TO_UNGROUP_NODE
PERM_EXPIRED_CHECK_PERIODIC = CONFIG.PERM_EXPIRED_CHECK_PERIODIC PERM_EXPIRED_CHECK_PERIODIC = CONFIG.PERM_EXPIRED_CHECK_PERIODIC
WINDOWS_SSH_DEFAULT_SHELL = CONFIG.WINDOWS_SSH_DEFAULT_SHELL WINDOWS_SSH_DEFAULT_SHELL = CONFIG.WINDOWS_SSH_DEFAULT_SHELL
@ -129,7 +124,11 @@ HEALTH_CHECK_TOKEN = CONFIG.HEALTH_CHECK_TOKEN
TERMINAL_RDP_ADDR = CONFIG.TERMINAL_RDP_ADDR TERMINAL_RDP_ADDR = CONFIG.TERMINAL_RDP_ADDR
SECURITY_LUNA_REMEMBER_AUTH = CONFIG.SECURITY_LUNA_REMEMBER_AUTH SECURITY_LUNA_REMEMBER_AUTH = CONFIG.SECURITY_LUNA_REMEMBER_AUTH
SECURITY_WATERMARK_ENABLED = CONFIG.SECURITY_WATERMARK_ENABLED SECURITY_WATERMARK_ENABLED = CONFIG.SECURITY_WATERMARK_ENABLED
SECURITY_SESSION_SHARE = CONFIG.SECURITY_SESSION_SHARE
LOGIN_REDIRECT_TO_BACKEND = CONFIG.LOGIN_REDIRECT_TO_BACKEND LOGIN_REDIRECT_TO_BACKEND = CONFIG.LOGIN_REDIRECT_TO_BACKEND
LOGIN_REDIRECT_MSG_ENABLED = CONFIG.LOGIN_REDIRECT_MSG_ENABLED
CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS = CONFIG.CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS = CONFIG.CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS
XRDP_ENABLED = CONFIG.XRDP_ENABLED

View File

@ -32,6 +32,7 @@ app_view_patterns = [
path('ops/', include('ops.urls.view_urls'), name='ops'), path('ops/', include('ops.urls.view_urls'), name='ops'),
path('common/', include('common.urls.view_urls'), name='common'), path('common/', include('common.urls.view_urls'), name='common'),
re_path(r'flower/(?P<path>.*)', views.celery_flower_view, name='flower-view'), re_path(r'flower/(?P<path>.*)', views.celery_flower_view, name='flower-view'),
path('download/', views.ResourceDownload.as_view(), name='download')
] ]
if settings.XPACK_ENABLED: if settings.XPACK_ENABLED:

View File

@ -4,7 +4,7 @@ import re
from django.http import HttpResponseRedirect, JsonResponse, Http404 from django.http import HttpResponseRedirect, JsonResponse, Http404
from django.conf import settings from django.conf import settings
from django.views.generic import View from django.views.generic import View, TemplateView
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
@ -16,7 +16,8 @@ from common.http import HttpResponseTemporaryRedirect
__all__ = [ __all__ = [
'LunaView', 'I18NView', 'KokoView', 'WsView', 'LunaView', 'I18NView', 'KokoView', 'WsView',
'redirect_format_api', 'redirect_old_apps_view', 'UIView' 'redirect_format_api', 'redirect_old_apps_view', 'UIView',
'ResourceDownload',
] ]
@ -84,3 +85,6 @@ class KokoView(View):
"</div>If you see this page, prove that you are not accessing the nginx listening port. Good luck.</div>") "</div>If you see this page, prove that you are not accessing the nginx listening port. Good luck.</div>")
return HttpResponse(msg) return HttpResponse(msg)
class ResourceDownload(TemplateView):
template_name = 'resource_download.html'

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,18 @@
from django.http import Http404 from rest_framework.mixins import ListModelMixin, UpdateModelMixin, RetrieveModelMixin
from rest_framework.mixins import ListModelMixin, UpdateModelMixin
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status
from common.drf.api import JMSGenericViewSet from common.drf.api import JMSGenericViewSet
from common.permissions import IsObjectOwner, IsSuperUser, OnlySuperUserCanList
from notifications.notifications import system_msgs from notifications.notifications import system_msgs
from notifications.models import SystemMsgSubscription from notifications.models import SystemMsgSubscription, UserMsgSubscription
from notifications.backends import BACKEND from notifications.backends import BACKEND
from notifications.serializers import ( from notifications.serializers import (
SystemMsgSubscriptionSerializer, SystemMsgSubscriptionByCategorySerializer SystemMsgSubscriptionSerializer, SystemMsgSubscriptionByCategorySerializer,
UserMsgSubscriptionSerializer,
) )
__all__ = ('BackendListView', 'SystemMsgSubscriptionViewSet') __all__ = ('BackendListView', 'SystemMsgSubscriptionViewSet', 'UserMsgSubscriptionViewSet')
class BackendListView(APIView): class BackendListView(APIView):
@ -70,3 +70,13 @@ class SystemMsgSubscriptionViewSet(ListModelMixin,
serializer = self.get_serializer(data, many=True) serializer = self.get_serializer(data, many=True)
return Response(data=serializer.data) return Response(data=serializer.data)
class UserMsgSubscriptionViewSet(ListModelMixin,
RetrieveModelMixin,
UpdateModelMixin,
JMSGenericViewSet):
lookup_field = 'user_id'
queryset = UserMsgSubscription.objects.all()
serializer_class = UserMsgSubscriptionSerializer
permission_classes = (IsObjectOwner | IsSuperUser, OnlySuperUserCanList)

View File

@ -1,11 +1,9 @@
import importlib
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.db import models from django.db import models
from .dingtalk import DingTalk client_name_mapper = {}
from .email import Email
from .site_msg import SiteMessage
from .wecom import WeCom
from .feishu import FeiShu
class BACKEND(models.TextChoices): class BACKEND(models.TextChoices):
@ -14,17 +12,11 @@ class BACKEND(models.TextChoices):
DINGTALK = 'dingtalk', _('DingTalk') DINGTALK = 'dingtalk', _('DingTalk')
SITE_MSG = 'site_msg', _('Site message') SITE_MSG = 'site_msg', _('Site message')
FEISHU = 'feishu', _('FeiShu') FEISHU = 'feishu', _('FeiShu')
SMS = 'sms', _('SMS')
@property @property
def client(self): def client(self):
client = { return client_name_mapper[self]
self.EMAIL: Email,
self.WECOM: WeCom,
self.DINGTALK: DingTalk,
self.SITE_MSG: SiteMessage,
self.FEISHU: FeiShu,
}[self]
return client
def get_account(self, user): def get_account(self, user):
return self.client.get_account(user) return self.client.get_account(user)
@ -37,3 +29,8 @@ class BACKEND(models.TextChoices):
def filter_enable_backends(cls, backends): def filter_enable_backends(cls, backends):
enable_backends = [b for b in backends if cls(b).is_enable] enable_backends = [b for b in backends if cls(b).is_enable]
return enable_backends return enable_backends
for b in BACKEND:
m = importlib.import_module(f'.{b}', __package__)
client_name_mapper[b] = m.backend

View File

@ -14,6 +14,9 @@ class DingTalk(BackendBase):
agentid=settings.DINGTALK_AGENTID agentid=settings.DINGTALK_AGENTID
) )
def send_msg(self, users, msg): def send_msg(self, users, message, subject=None):
accounts, __, __ = self.get_accounts(users) accounts, __, __ = self.get_accounts(users)
return self.dingtalk.send_text(accounts, msg) return self.dingtalk.send_text(accounts, message)
backend = DingTalk

View File

@ -8,7 +8,10 @@ class Email(BackendBase):
account_field = 'email' account_field = 'email'
is_enable_field_in_settings = 'EMAIL_HOST_USER' is_enable_field_in_settings = 'EMAIL_HOST_USER'
def send_msg(self, users, subject, message): def send_msg(self, users, message, subject):
from_email = settings.EMAIL_FROM or settings.EMAIL_HOST_USER from_email = settings.EMAIL_FROM or settings.EMAIL_HOST_USER
accounts, __, __ = self.get_accounts(users) accounts, __, __ = self.get_accounts(users)
send_mail(subject, message, from_email, accounts, html_message=message) send_mail(subject, message, from_email, accounts, html_message=message)
backend = Email

View File

@ -14,6 +14,9 @@ class FeiShu(BackendBase):
app_secret=settings.FEISHU_APP_SECRET app_secret=settings.FEISHU_APP_SECRET
) )
def send_msg(self, users, msg): def send_msg(self, users, message, subject=None):
accounts, __, __ = self.get_accounts(users) accounts, __, __ = self.get_accounts(users)
return self.client.send_text(accounts, msg) return self.client.send_text(accounts, message)
backend = FeiShu

View File

@ -5,10 +5,13 @@ from .base import BackendBase
class SiteMessage(BackendBase): class SiteMessage(BackendBase):
account_field = 'id' account_field = 'id'
def send_msg(self, users, subject, message): def send_msg(self, users, message, subject):
accounts, __, __ = self.get_accounts(users) accounts, __, __ = self.get_accounts(users)
Client.send_msg(subject, message, user_ids=accounts) Client.send_msg(subject, message, user_ids=accounts)
@classmethod @classmethod
def is_enable(cls): def is_enable(cls):
return True return True
backend = SiteMessage

View File

@ -0,0 +1,25 @@
from django.conf import settings
from common.message.backends.sms.alibaba import AlibabaSMS as Client
from .base import BackendBase
class SMS(BackendBase):
account_field = 'phone'
is_enable_field_in_settings = 'SMS_ENABLED'
def __init__(self):
"""
暂时只对接阿里之后再扩展
"""
self.client = Client(
access_key_id=settings.ALIBABA_ACCESS_KEY_ID,
access_key_secret=settings.ALIBABA_ACCESS_KEY_SECRET
)
def send_msg(self, users, sign_name: str, template_code: str, template_param: dict):
accounts, __, __ = self.get_accounts(users)
return self.client.send_sms(accounts, sign_name, template_code, template_param)
backend = SMS

View File

@ -15,6 +15,9 @@ class WeCom(BackendBase):
agentid=settings.WECOM_AGENTID agentid=settings.WECOM_AGENTID
) )
def send_msg(self, users, msg): def send_msg(self, users, message, subject=None):
accounts, __, __ = self.get_accounts(users) accounts, __, __ = self.get_accounts(users)
return self.wecom.send_text(accounts, msg) return self.wecom.send_text(accounts, message)
backend = WeCom

View File

@ -44,7 +44,7 @@ class Migration(migrations.Migration):
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('message_type', models.CharField(max_length=128)), ('message_type', models.CharField(max_length=128)),
('receive_backends', models.JSONField(default=list)), ('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)), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_msg_subscription', to=settings.AUTH_USER_MODEL)),
], ],
options={ options={
'abstract': False, 'abstract': False,

View File

@ -0,0 +1,55 @@
# Generated by Django 3.1.12 on 2021-09-09 11:46
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
def init_user_msg_subscription(apps, schema_editor):
UserMsgSubscription = apps.get_model('notifications', 'UserMsgSubscription')
User = apps.get_model('users', 'User')
to_create = []
users = User.objects.all()
for user in users:
receive_backends = []
receive_backends.append('site_msg')
if user.email:
receive_backends.append('email')
if user.wecom_id:
receive_backends.append('wecom')
if user.dingtalk_id:
receive_backends.append('dingtalk')
if user.feishu_id:
receive_backends.append('feishu')
to_create.append(UserMsgSubscription(user=user, receive_backends=receive_backends))
UserMsgSubscription.objects.bulk_create(to_create)
print(f'\n Init user message subscription: {len(to_create)}')
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('notifications', '0001_initial'),
('users', '0036_user_feishu_id'),
]
operations = [
migrations.RemoveField(
model_name='usermsgsubscription',
name='message_type',
),
migrations.AlterField(
model_name='usermsgsubscription',
name='user',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='user_msg_subscription', to=settings.AUTH_USER_MODEL),
),
migrations.RunPython(init_user_msg_subscription)
]

View File

@ -6,12 +6,11 @@ __all__ = ('SystemMsgSubscription', 'UserMsgSubscription')
class UserMsgSubscription(JMSModel): class UserMsgSubscription(JMSModel):
message_type = models.CharField(max_length=128) user = models.OneToOneField('users.User', related_name='user_msg_subscription', on_delete=models.CASCADE)
user = models.ForeignKey('users.User', related_name='user_msg_subscriptions', on_delete=models.CASCADE)
receive_backends = models.JSONField(default=list) receive_backends = models.JSONField(default=list)
def __str__(self): def __str__(self):
return f'{self.message_type}' return f'{self.user} subscription: {self.receive_backends}'
class SystemMsgSubscription(JMSModel): class SystemMsgSubscription(JMSModel):

View File

@ -1,14 +1,16 @@
from typing import Iterable from typing import Iterable
import traceback import traceback
from itertools import chain from itertools import chain
from collections import defaultdict
from django.db.utils import ProgrammingError
from celery import shared_task from celery import shared_task
from common.utils import lazyproperty
from users.models import User
from notifications.backends import BACKEND from notifications.backends import BACKEND
from .models import SystemMsgSubscription from .models import SystemMsgSubscription, UserMsgSubscription
__all__ = ('SystemMessage', 'UserMessage') __all__ = ('SystemMessage', 'UserMessage', 'system_msgs')
system_msgs = [] system_msgs = []
@ -66,40 +68,55 @@ class Message(metaclass=MessageType):
raise NotImplementedError raise NotImplementedError
def send_msg(self, users: Iterable, backends: Iterable = BACKEND): def send_msg(self, users: Iterable, backends: Iterable = BACKEND):
backends = set(backends)
backends.add(BACKEND.SITE_MSG) # 站内信必须发
for backend in backends: for backend in backends:
try: try:
backend = BACKEND(backend) backend = BACKEND(backend)
if not backend.is_enable:
continue
get_msg_method = getattr(self, f'get_{backend}_msg', self.get_common_msg) get_msg_method = getattr(self, f'get_{backend}_msg', self.get_common_msg)
msg = get_msg_method()
try:
msg = get_msg_method()
except NotImplementedError:
continue
client = backend.client() client = backend.client()
if isinstance(msg, dict): client.send_msg(users, **msg)
client.send_msg(users, **msg)
else:
client.send_msg(users, msg)
except: except:
traceback.print_exc() traceback.print_exc()
def get_common_msg(self) -> str: def get_common_msg(self) -> dict:
raise NotImplementedError raise NotImplementedError
def get_dingtalk_msg(self) -> str: @lazyproperty
def common_msg(self) -> dict:
return self.get_common_msg() return self.get_common_msg()
def get_wecom_msg(self) -> str: # --------------------------------------------------------------
return self.get_common_msg() # 支持不同发送消息的方式定义自己的消息内容,比如有些支持 html 标签
def get_dingtalk_msg(self) -> dict:
return self.common_msg
def get_wecom_msg(self) -> dict:
return self.common_msg
def get_feishu_msg(self) -> dict:
return self.common_msg
def get_email_msg(self) -> dict: def get_email_msg(self) -> dict:
msg = self.get_common_msg() return self.common_msg
subject = f'{msg[:80]} ...' if len(msg) >= 80 else msg
return {
'subject': subject,
'message': msg
}
def get_site_msg_msg(self) -> dict: def get_site_msg_msg(self) -> dict:
return self.get_email_msg() return self.common_msg
def get_sms_msg(self) -> dict:
raise NotImplementedError
# --------------------------------------------------------------
class SystemMessage(Message): class SystemMessage(Message):
@ -125,4 +142,16 @@ class SystemMessage(Message):
class UserMessage(Message): class UserMessage(Message):
pass user: User
def __init__(self, user):
self.user = user
def publish(self):
"""
发送消息到每个用户配置的接收方式上
"""
sub = UserMsgSubscription.objects.get(user=self.user)
self.send_msg([self.user], sub.receive_backends)

View File

@ -1,7 +1,7 @@
from rest_framework import serializers from rest_framework import serializers
from common.drf.serializers import BulkModelSerializer from common.drf.serializers import BulkModelSerializer
from notifications.models import SystemMsgSubscription from notifications.models import SystemMsgSubscription, UserMsgSubscription
class SystemMsgSubscriptionSerializer(BulkModelSerializer): class SystemMsgSubscriptionSerializer(BulkModelSerializer):
@ -27,3 +27,11 @@ class SystemMsgSubscriptionByCategorySerializer(serializers.Serializer):
category = serializers.CharField() category = serializers.CharField()
category_label = serializers.CharField() category_label = serializers.CharField()
children = SystemMsgSubscriptionSerializer(many=True) children = SystemMsgSubscriptionSerializer(many=True)
class UserMsgSubscriptionSerializer(BulkModelSerializer):
receive_backends = serializers.ListField(child=serializers.CharField(), read_only=False)
class Meta:
model = UserMsgSubscription
fields = ('user_id', 'receive_backends',)

View File

@ -6,14 +6,14 @@ from django.utils.functional import LazyObject
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.db.models.signals import post_migrate from django.db.models.signals import post_migrate
from django.dispatch import receiver from django.dispatch import receiver
from django.db.utils import DEFAULT_DB_ALIAS
from django.apps import apps as global_apps
from django.apps import AppConfig from django.apps import AppConfig
from notifications.backends import BACKEND
from users.models import User
from common.utils.connection import RedisPubSub from common.utils.connection import RedisPubSub
from common.utils import get_logger from common.utils import get_logger
from common.decorator import on_transaction_commit from common.decorator import on_transaction_commit
from .models import SiteMessage, SystemMsgSubscription from .models import SiteMessage, SystemMsgSubscription, UserMsgSubscription
from .notifications import SystemMessage from .notifications import SystemMessage
@ -82,3 +82,13 @@ def create_system_messages(app_config: AppConfig, **kwargs):
logger.info(f'Create SystemMsgSubscription: package={app_config.module.__package__} type={message_type}') logger.info(f'Create SystemMsgSubscription: package={app_config.module.__package__} type={message_type}')
except ModuleNotFoundError: except ModuleNotFoundError:
pass pass
@receiver(post_save, sender=User)
def on_user_post_save(sender, instance, created, **kwargs):
if created:
receive_backends = []
for backend in BACKEND:
if backend.get_account(instance):
receive_backends.append(backend)
UserMsgSubscription.objects.create(user=instance, receive_backends=receive_backends)

View File

@ -2,9 +2,12 @@ from django.db.models import F
from django.db import transaction from django.db import transaction
from common.utils.timezone import now from common.utils.timezone import now
from common.utils import get_logger
from users.models import User from users.models import User
from .models import SiteMessage as SiteMessageModel, SiteMessageUsers from .models import SiteMessage as SiteMessageModel, SiteMessageUsers
logger = get_logger(__file__)
class SiteMessageUtil: class SiteMessageUtil:
@ -14,6 +17,11 @@ class SiteMessageUtil:
if not any((user_ids, group_ids, is_broadcast)): if not any((user_ids, group_ids, is_broadcast)):
raise ValueError('No recipient is specified') raise ValueError('No recipient is specified')
logger.info(f'Site message send: '
f'user_ids={user_ids} '
f'group_ids={group_ids} '
f'subject={subject} '
f'message={message}')
with transaction.atomic(): with transaction.atomic():
site_msg = SiteMessageModel.objects.create( site_msg = SiteMessageModel.objects.create(
subject=subject, message=message, subject=subject, message=message,

View File

@ -8,6 +8,7 @@ app_name = 'notifications'
router = BulkRouter() router = BulkRouter()
router.register('system-msg-subscription', api.SystemMsgSubscriptionViewSet, 'system-msg-subscription') router.register('system-msg-subscription', api.SystemMsgSubscriptionViewSet, 'system-msg-subscription')
router.register('user-msg-subscription', api.UserMsgSubscriptionViewSet, 'user-msg-subscription')
router.register('site-message', api.SiteMessageViewSet, 'site-message') router.register('site-message', api.SiteMessageViewSet, 'site-message')
urlpatterns = [ urlpatterns = [

View File

@ -4,7 +4,6 @@ from redis.exceptions import ConnectionError
from channels.generic.websocket import JsonWebsocketConsumer from channels.generic.websocket import JsonWebsocketConsumer
from common.utils import get_logger from common.utils import get_logger
from .models import SiteMessage
from .site_msg import SiteMessageUtil from .site_msg import SiteMessageUtil
from .signals_handler import new_site_msg_chan from .signals_handler import new_site_msg_chan

View File

@ -46,7 +46,7 @@ class BaseHost(Host):
if host_data.get('username'): if host_data.get('username'):
self.set_variable('ansible_user', host_data['username']) self.set_variable('ansible_user', host_data['username'])
# 添加密码和 # 添加密码和
if host_data.get('password'): if host_data.get('password'):
self.set_variable('ansible_ssh_pass', host_data['password']) self.set_variable('ansible_ssh_pass', host_data['password'])
if host_data.get('private_key'): if host_data.get('private_key'):

View File

@ -159,7 +159,7 @@ class PeriodTaskFormMixin(forms.Form):
) )
interval = forms.IntegerField( interval = forms.IntegerField(
required=False, initial=24, required=False, initial=24,
help_text=_('Tips: (Units: hour)'), label=_("Cycle perform"), help_text=_('Unit: hour'), label=_("Cycle perform"),
) )
def get_initial_for_field(self, field, field_name): def get_initial_for_field(self, field, field_name):

View File

@ -18,7 +18,10 @@ class ServerPerformanceMessage(SystemMessage):
self._msg = msg self._msg = msg
def get_common_msg(self): def get_common_msg(self):
return self._msg return {
'subject': self._msg[:80],
'message': self._msg
}
@classmethod @classmethod
def post_insert_to_db(cls, subscription: SystemMsgSubscription): def post_insert_to_db(cls, subscription: SystemMsgSubscription):

View File

@ -19,6 +19,7 @@ __all__ = [
class OrgManager(models.Manager): class OrgManager(models.Manager):
def all_group_by_org(self): def all_group_by_org(self):
from ..models import Organization from ..models import Organization
orgs = list(Organization.objects.all()) orgs = list(Organization.objects.all())

View File

@ -25,7 +25,7 @@ class ResourceStatisticsSerializer(serializers.Serializer):
app_perms_amount = serializers.IntegerField(required=False) app_perms_amount = serializers.IntegerField(required=False)
class OrgSerializer(ModelSerializer): class OrgSerializer(BulkModelSerializer):
users = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True, required=False) users = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True, required=False)
admins = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True, required=False) admins = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True, required=False)
auditors = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True, required=False) auditors = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True, required=False)

View File

@ -19,8 +19,8 @@ class UserGroupGrantedApplicationsApi(CommonApiMixin, ListAPIView):
获取用户组直接授权的应用 获取用户组直接授权的应用
""" """
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.ApplicationGrantedSerializer serializer_class = serializers.AppGrantedSerializer
only_fields = serializers.ApplicationGrantedSerializer.Meta.only_fields only_fields = serializers.AppGrantedSerializer.Meta.only_fields
filterset_fields = ['id', 'name', 'category', 'type', 'comment'] filterset_fields = ['id', 'name', 'category', 'type', 'comment']
search_fields = ['name', 'comment'] search_fields = ['name', 'comment']

View File

@ -24,8 +24,8 @@ __all__ = [
class AllGrantedApplicationsMixin(CommonApiMixin, ListAPIView): class AllGrantedApplicationsMixin(CommonApiMixin, ListAPIView):
only_fields = serializers.ApplicationGrantedSerializer.Meta.only_fields only_fields = serializers.AppGrantedSerializer.Meta.only_fields
serializer_class = serializers.ApplicationGrantedSerializer serializer_class = serializers.AppGrantedSerializer
filterset_fields = { filterset_fields = {
'id': ['exact'], 'id': ['exact'],
'name': ['exact'], 'name': ['exact'],

4
apps/perms/const.py Normal file
View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
#
from django.db.models import TextChoices
from django.utils.translation import ugettext_lazy as _

View File

@ -0,0 +1,23 @@
# Generated by Django 3.1.12 on 2021-09-06 02:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('perms', '0018_auto_20210208_1515'),
]
operations = [
migrations.AddField(
model_name='applicationpermission',
name='from_ticket',
field=models.BooleanField(default=False, verbose_name='From ticket'),
),
migrations.AddField(
model_name='assetpermission',
name='from_ticket',
field=models.BooleanField(default=False, verbose_name='From ticket'),
),
]

View File

@ -12,7 +12,6 @@ from common.db.models import UnionQuerySet
from common.utils import date_expired_default, lazyproperty from common.utils import date_expired_default, lazyproperty
from orgs.mixins.models import OrgManager from orgs.mixins.models import OrgManager
__all__ = [ __all__ = [
'BasePermission', 'BasePermissionQuerySet' 'BasePermission', 'BasePermissionQuerySet'
] ]
@ -31,11 +30,7 @@ class BasePermissionQuerySet(models.QuerySet):
def invalid(self): def invalid(self):
now = timezone.now() now = timezone.now()
q = ( q = (Q(is_active=False) | Q(date_start__gt=now) | Q(date_expired__lt=now))
Q(is_active=False) |
Q(date_start__gt=now) |
Q(date_expired__lt=now)
)
return self.filter(q) return self.filter(q)
@ -48,13 +43,15 @@ class BasePermission(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_('Name')) name = models.CharField(max_length=128, verbose_name=_('Name'))
users = models.ManyToManyField('users.User', blank=True, verbose_name=_("User"), related_name='%(class)ss') users = models.ManyToManyField('users.User', blank=True, verbose_name=_("User"), related_name='%(class)ss')
user_groups = models.ManyToManyField('users.UserGroup', blank=True, verbose_name=_("User group"), related_name='%(class)ss') user_groups = models.ManyToManyField(
'users.UserGroup', blank=True, verbose_name=_("User group"), related_name='%(class)ss')
is_active = models.BooleanField(default=True, verbose_name=_('Active')) is_active = models.BooleanField(default=True, verbose_name=_('Active'))
date_start = models.DateTimeField(default=timezone.now, db_index=True, verbose_name=_("Date start")) date_start = models.DateTimeField(default=timezone.now, db_index=True, verbose_name=_("Date start"))
date_expired = models.DateTimeField(default=date_expired_default, db_index=True, verbose_name=_('Date expired')) date_expired = models.DateTimeField(default=date_expired_default, db_index=True, verbose_name=_('Date expired'))
created_by = models.CharField(max_length=128, blank=True, verbose_name=_('Created by')) created_by = models.CharField(max_length=128, blank=True, verbose_name=_('Created by'))
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created')) date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created'))
comment = models.TextField(verbose_name=_('Comment'), blank=True) comment = models.TextField(verbose_name=_('Comment'), blank=True)
from_ticket = models.BooleanField(default=False, verbose_name=_('From ticket'))
objects = BasePermissionManager.from_queryset(BasePermissionQuerySet)() objects = BasePermissionManager.from_queryset(BasePermissionQuerySet)()

View File

@ -24,7 +24,7 @@ class ApplicationPermissionSerializer(BulkOrgResourceModelSerializer):
fields_small = fields_mini + [ fields_small = fields_mini + [
'category', 'category_display', 'type', 'type_display', 'category', 'category_display', 'type', 'type_display',
'is_active', 'is_expired', 'is_valid', 'is_active', 'is_expired', 'is_valid',
'created_by', 'date_created', 'date_expired', 'date_start', 'comment' 'created_by', 'date_created', 'date_expired', 'date_start', 'comment', 'from_ticket'
] ]
fields_m2m = [ fields_m2m = [
'users', 'user_groups', 'applications', 'system_users', 'users', 'user_groups', 'applications', 'system_users',
@ -32,7 +32,7 @@ class ApplicationPermissionSerializer(BulkOrgResourceModelSerializer):
'system_users_amount', 'system_users_amount',
] ]
fields = fields_small + fields_m2m fields = fields_small + fields_m2m
read_only_fields = ['created_by', 'date_created'] read_only_fields = ['created_by', 'date_created', 'from_ticket']
extra_kwargs = { extra_kwargs = {
'is_expired': {'label': _('Is expired')}, 'is_expired': {'label': _('Is expired')},
'is_valid': {'label': _('Is valid')}, 'is_valid': {'label': _('Is valid')},

View File

@ -6,10 +6,10 @@ from django.utils.translation import ugettext_lazy as _
from assets.models import SystemUser from assets.models import SystemUser
from applications.models import Application from applications.models import Application
from applications.serializers import ApplicationSerializerMixin from applications.serializers import AppSerializerMixin
__all__ = [ __all__ = [
'ApplicationGrantedSerializer', 'ApplicationSystemUserSerializer' 'AppGrantedSerializer', 'ApplicationSystemUserSerializer'
] ]
@ -26,7 +26,7 @@ class ApplicationSystemUserSerializer(serializers.ModelSerializer):
read_only_fields = fields read_only_fields = fields
class ApplicationGrantedSerializer(ApplicationSerializerMixin, serializers.ModelSerializer): class AppGrantedSerializer(AppSerializerMixin, serializers.ModelSerializer):
""" """
被授权应用的数据结构 被授权应用的数据结构
""" """

View File

@ -53,7 +53,7 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
fields_small = fields_mini + [ fields_small = fields_mini + [
'is_active', 'is_expired', 'is_valid', 'actions', 'is_active', 'is_expired', 'is_valid', 'actions',
'created_by', 'date_created', 'date_expired', 'created_by', 'date_created', 'date_expired',
'date_start', 'comment' 'date_start', 'comment', 'from_ticket'
] ]
fields_m2m = [ fields_m2m = [
'users', 'users_display', 'user_groups', 'user_groups_display', 'assets', 'users', 'users_display', 'user_groups', 'user_groups_display', 'assets',
@ -62,7 +62,7 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer):
'nodes_amount', 'system_users_amount', 'nodes_amount', 'system_users_amount',
] ]
fields = fields_small + fields_m2m fields = fields_small + fields_m2m
read_only_fields = ['created_by', 'date_created'] read_only_fields = ['created_by', 'date_created', 'from_ticket']
extra_kwargs = { extra_kwargs = {
'is_expired': {'label': _('Is expired')}, 'is_expired': {'label': _('Is expired')},
'is_valid': {'label': _('Is valid')}, 'is_valid': {'label': _('Is valid')},

View File

@ -1,2 +1,3 @@
from . import common from . import asset_permission
from . import app_permission
from . import refresh_perms from . import refresh_perms

View File

@ -0,0 +1,104 @@
import itertools
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
from users.models import User, UserGroup
from assets.models import SystemUser
from applications.models import Application
from common.utils import get_logger
from common.exceptions import M2MReverseNotAllowed
from common.decorator import on_transaction_commit
from common.const.signals import POST_ADD
from perms.models import ApplicationPermission
from applications.models import Account as AppAccount
logger = get_logger(__file__)
@receiver(m2m_changed, sender=ApplicationPermission.applications.through)
@on_transaction_commit
def on_app_permission_applications_changed(sender, instance, action, reverse, pk_set, **kwargs):
if reverse:
raise M2MReverseNotAllowed
if action != POST_ADD:
return
logger.debug("Application permission applications change signal received")
system_users = instance.system_users.all()
set_remote_app_asset_system_users_if_need(instance, system_users=system_users)
apps = Application.objects.filter(pk__in=pk_set)
set_app_accounts(apps, system_users)
def set_app_accounts(apps, system_users):
for app, system_user in itertools.product(apps, system_users):
AppAccount.objects.get_or_create(
defaults={'app': app, 'systemuser': system_user},
app=app, systemuser=system_user
)
def set_remote_app_asset_system_users_if_need(instance: ApplicationPermission, system_users=None,
users=None, groups=None):
if not instance.category_remote_app:
return
attrs = instance.applications.all().values_list('attrs', flat=True)
asset_ids = [attr['asset'] for attr in attrs if attr.get('asset')]
if not asset_ids:
return
system_users = system_users or instance.system_users.all()
for system_user in system_users:
system_user.assets.add(*asset_ids)
if system_user.username_same_with_user:
users = users or instance.users.all()
groups = groups or instance.user_groups.all()
system_user.groups.add(*groups)
system_user.users.add(*users)
@receiver(m2m_changed, sender=ApplicationPermission.system_users.through)
@on_transaction_commit
def on_app_permission_system_users_changed(sender, instance, action, reverse, pk_set, **kwargs):
if reverse:
raise M2MReverseNotAllowed
if action != POST_ADD:
return
logger.debug("Application permission system_users change signal received")
system_users = SystemUser.objects.filter(pk__in=pk_set)
set_remote_app_asset_system_users_if_need(instance, system_users=system_users)
apps = instance.applications.all()
set_app_accounts(apps, system_users)
@receiver(m2m_changed, sender=ApplicationPermission.users.through)
@on_transaction_commit
def on_app_permission_users_changed(sender, instance, action, reverse, pk_set, **kwargs):
if reverse:
raise M2MReverseNotAllowed
if action != POST_ADD:
return
logger.debug("Application permission users change signal received")
users = User.objects.filter(pk__in=pk_set)
set_remote_app_asset_system_users_if_need(instance, users=users)
@receiver(m2m_changed, sender=ApplicationPermission.user_groups.through)
@on_transaction_commit
def on_app_permission_user_groups_changed(sender, instance, action, reverse, pk_set, **kwargs):
if reverse:
raise M2MReverseNotAllowed
if action != POST_ADD:
return
logger.debug("Application permission user groups change signal received")
groups = UserGroup.objects.filter(pk__in=pk_set)
set_remote_app_asset_system_users_if_need(instance, groups=groups)

View File

@ -3,19 +3,20 @@
from django.db.models.signals import m2m_changed from django.db.models.signals import m2m_changed
from django.dispatch import receiver from django.dispatch import receiver
from users.models import User, UserGroup from users.models import User
from assets.models import SystemUser from assets.models import SystemUser
from applications.models import Application
from common.utils import get_logger from common.utils import get_logger
from common.decorator import on_transaction_commit
from common.exceptions import M2MReverseNotAllowed from common.exceptions import M2MReverseNotAllowed
from common.const.signals import POST_ADD from common.const.signals import POST_ADD
from perms.models import AssetPermission, ApplicationPermission from perms.models import AssetPermission
logger = get_logger(__file__) logger = get_logger(__file__)
@receiver(m2m_changed, sender=User.groups.through) @receiver(m2m_changed, sender=User.groups.through)
@on_transaction_commit
def on_user_groups_change(sender, instance, action, reverse, pk_set, **kwargs): def on_user_groups_change(sender, instance, action, reverse, pk_set, **kwargs):
""" """
UserGroup 增加 User 增加的 User 需要与 UserGroup 关联的动态系统用户相关联 UserGroup 增加 User 增加的 User 需要与 UserGroup 关联的动态系统用户相关联
@ -39,12 +40,13 @@ def on_user_groups_change(sender, instance, action, reverse, pk_set, **kwargs):
@receiver(m2m_changed, sender=AssetPermission.nodes.through) @receiver(m2m_changed, sender=AssetPermission.nodes.through)
@on_transaction_commit
def on_permission_nodes_changed(instance, action, reverse, pk_set, model, **kwargs): def on_permission_nodes_changed(instance, action, reverse, pk_set, model, **kwargs):
if reverse: if reverse:
raise M2MReverseNotAllowed raise M2MReverseNotAllowed
if action != POST_ADD: if action != POST_ADD:
return return
logger.debug("Asset permission nodes change signal received") logger.debug("Asset permission nodes change signal received")
nodes = model.objects.filter(pk__in=pk_set) nodes = model.objects.filter(pk__in=pk_set)
system_users = instance.system_users.all() system_users = instance.system_users.all()
@ -55,12 +57,13 @@ def on_permission_nodes_changed(instance, action, reverse, pk_set, model, **kwar
@receiver(m2m_changed, sender=AssetPermission.assets.through) @receiver(m2m_changed, sender=AssetPermission.assets.through)
@on_transaction_commit
def on_permission_assets_changed(instance, action, reverse, pk_set, model, **kwargs): def on_permission_assets_changed(instance, action, reverse, pk_set, model, **kwargs):
if reverse: if reverse:
raise M2MReverseNotAllowed raise M2MReverseNotAllowed
if action != POST_ADD: if action != POST_ADD:
return return
logger.debug("Asset permission assets change signal received") logger.debug("Asset permission assets change signal received")
assets = model.objects.filter(pk__in=pk_set) assets = model.objects.filter(pk__in=pk_set)
@ -71,33 +74,38 @@ def on_permission_assets_changed(instance, action, reverse, pk_set, model, **kwa
@receiver(m2m_changed, sender=AssetPermission.system_users.through) @receiver(m2m_changed, sender=AssetPermission.system_users.through)
@on_transaction_commit
def on_asset_permission_system_users_changed(instance, action, reverse, **kwargs): def on_asset_permission_system_users_changed(instance, action, reverse, **kwargs):
if reverse: if reverse:
raise M2MReverseNotAllowed raise M2MReverseNotAllowed
if action != POST_ADD: if action != POST_ADD:
return return
logger.debug("Asset permission system_users change signal received") logger.debug("Asset permission system_users change signal received")
system_users = kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) system_users = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
assets = instance.assets.all().values_list('id', flat=True) assets = instance.assets.all().values_list('id', flat=True)
nodes = instance.nodes.all().values_list('id', flat=True) nodes = instance.nodes.all().values_list('id', flat=True)
users = instance.users.all().values_list('id', flat=True)
groups = instance.user_groups.all().values_list('id', flat=True)
for system_user in system_users: for system_user in system_users:
system_user.nodes.add(*tuple(nodes)) system_user.nodes.add(*tuple(nodes))
system_user.assets.add(*tuple(assets)) system_user.assets.add(*tuple(assets))
# 动态系统用户,需要关联用户和用户组了
if system_user.username_same_with_user: if system_user.username_same_with_user:
users = instance.users.all().values_list('id', flat=True)
groups = instance.user_groups.all().values_list('id', flat=True)
system_user.groups.add(*tuple(groups)) system_user.groups.add(*tuple(groups))
system_user.users.add(*tuple(users)) system_user.users.add(*tuple(users))
@receiver(m2m_changed, sender=AssetPermission.users.through) @receiver(m2m_changed, sender=AssetPermission.users.through)
@on_transaction_commit
def on_asset_permission_users_changed(instance, action, reverse, pk_set, model, **kwargs): def on_asset_permission_users_changed(instance, action, reverse, pk_set, model, **kwargs):
if reverse: if reverse:
raise M2MReverseNotAllowed raise M2MReverseNotAllowed
if action != POST_ADD: if action != POST_ADD:
return return
logger.debug("Asset permission users change signal received") logger.debug("Asset permission users change signal received")
users = model.objects.filter(pk__in=pk_set) users = model.objects.filter(pk__in=pk_set)
system_users = instance.system_users.all() system_users = instance.system_users.all()
@ -109,13 +117,13 @@ def on_asset_permission_users_changed(instance, action, reverse, pk_set, model,
@receiver(m2m_changed, sender=AssetPermission.user_groups.through) @receiver(m2m_changed, sender=AssetPermission.user_groups.through)
def on_asset_permission_user_groups_changed(instance, action, pk_set, model, @on_transaction_commit
reverse, **kwargs): def on_asset_permission_user_groups_changed(instance, action, pk_set, model, reverse, **kwargs):
if reverse: if reverse:
raise M2MReverseNotAllowed raise M2MReverseNotAllowed
if action != POST_ADD: if action != POST_ADD:
return return
logger.debug("Asset permission user groups change signal received") logger.debug("Asset permission user groups change signal received")
groups = model.objects.filter(pk__in=pk_set) groups = model.objects.filter(pk__in=pk_set)
system_users = instance.system_users.all() system_users = instance.system_users.all()
@ -126,87 +134,6 @@ def on_asset_permission_user_groups_changed(instance, action, pk_set, model,
system_user.groups.add(*tuple(groups)) system_user.groups.add(*tuple(groups))
@receiver(m2m_changed, sender=ApplicationPermission.system_users.through)
def on_application_permission_system_users_changed(sender, instance: ApplicationPermission, action, reverse, pk_set, **kwargs):
if reverse:
raise M2MReverseNotAllowed
if not instance.category_remote_app:
return
if action != POST_ADD:
return
system_users = SystemUser.objects.filter(pk__in=pk_set)
logger.debug("Application permission system_users change signal received")
attrs = instance.applications.all().values_list('attrs', flat=True)
asset_ids = [attr['asset'] for attr in attrs if attr.get('asset')]
if not asset_ids:
return
for system_user in system_users:
system_user.assets.add(*asset_ids)
if system_user.username_same_with_user:
user_ids = instance.users.all().values_list('id', flat=True)
group_ids = instance.user_groups.all().values_list('id', flat=True)
system_user.groups.add(*group_ids)
system_user.users.add(*user_ids)
@receiver(m2m_changed, sender=ApplicationPermission.users.through)
def on_application_permission_users_changed(sender, instance, action, reverse, pk_set, **kwargs):
if reverse:
raise M2MReverseNotAllowed
if not instance.category_remote_app:
return
if action != POST_ADD:
return
logger.debug("Application permission users change signal received")
user_ids = User.objects.filter(pk__in=pk_set).values_list('id', flat=True)
system_users = instance.system_users.all()
for system_user in system_users:
if system_user.username_same_with_user:
system_user.users.add(*user_ids)
@receiver(m2m_changed, sender=ApplicationPermission.user_groups.through)
def on_application_permission_user_groups_changed(sender, instance, action, reverse, pk_set, **kwargs):
if reverse:
raise M2MReverseNotAllowed
if not instance.category_remote_app:
return
if action != POST_ADD:
return
logger.debug("Application permission user groups change signal received")
group_ids = UserGroup.objects.filter(pk__in=pk_set).values_list('id', flat=True)
system_users = instance.system_users.all()
for system_user in system_users:
if system_user.username_same_with_user:
system_user.groups.add(*group_ids)
@receiver(m2m_changed, sender=ApplicationPermission.applications.through)
def on_application_permission_applications_changed(sender, instance, action, reverse, pk_set, **kwargs):
if reverse:
raise M2MReverseNotAllowed
if not instance.category_remote_app:
return
if action != POST_ADD:
return
attrs = Application.objects.filter(id__in=pk_set).values_list('attrs', flat=True)
asset_ids = [attr['asset'] for attr in attrs if attr.get('asset')]
if not asset_ids:
return
system_users = instance.system_users.all()
for system_user in system_users:
system_user.assets.add(*asset_ids)

View File

@ -1,5 +1,10 @@
from .common import * from .settings import *
from .ldap import * from .ldap import *
from .wecom import * from .wecom import *
from .dingtalk import * from .dingtalk import *
from .feishu import * from .feishu import *
from .public import *
from .email import *
from .alibaba_sms import *
from .tencent_sms import *
from .sms import *

View File

@ -0,0 +1,58 @@
from rest_framework.views import Response
from rest_framework.generics import GenericAPIView
from rest_framework.exceptions import APIException
from rest_framework import status
from django.utils.translation import gettext_lazy as _
from common.message.backends.sms import SMS_MESSAGE
from common.message.backends.sms.alibaba import AlibabaSMS
from settings.models import Setting
from common.permissions import IsSuperUser
from common.exceptions import JMSException
from .. import serializers
class AlibabaSMSTestingAPI(GenericAPIView):
permission_classes = (IsSuperUser,)
serializer_class = serializers.AlibabaSMSSettingSerializer
def post(self, request):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
alibaba_access_key_id = serializer.validated_data['ALIBABA_ACCESS_KEY_ID']
alibaba_access_key_secret = serializer.validated_data.get('ALIBABA_ACCESS_KEY_SECRET')
alibaba_sms_sign_and_tmpl = serializer.validated_data['ALIBABA_SMS_SIGN_AND_TEMPLATES']
test_phone = serializer.validated_data.get('SMS_TEST_PHONE')
if not test_phone:
raise JMSException(code='test_phone_required', detail=_('test_phone is required'))
if not alibaba_access_key_secret:
secret = Setting.objects.filter(name='ALIBABA_ACCESS_KEY_SECRET').first()
if secret:
alibaba_access_key_secret = secret.cleaned_value
alibaba_access_key_secret = alibaba_access_key_secret or ''
try:
client = AlibabaSMS(
access_key_id=alibaba_access_key_id,
access_key_secret=alibaba_access_key_secret
)
sign, tmpl = SMS_MESSAGE.VERIFICATION_CODE.get_sign_and_tmpl(alibaba_sms_sign_and_tmpl)
client.send_sms(
phone_numbers=[test_phone],
sign_name=sign,
template_code=tmpl,
template_param={'code': 'test'}
)
return Response(status=status.HTTP_200_OK, data={'msg': _('Test success')})
except APIException as e:
try:
error = e.detail['errmsg']
except:
error = e.detail
return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': error})

View File

@ -1,192 +0,0 @@
# -*- coding: utf-8 -*-
#
from smtplib import SMTPSenderRefused
from rest_framework import generics
from rest_framework.views import Response, APIView
from rest_framework.permissions import AllowAny
from django.conf import settings
from django.core.mail import send_mail, get_connection
from django.utils.translation import ugettext_lazy as _
from django.templatetags.static import static
from jumpserver.utils import has_valid_xpack_license
from common.permissions import IsSuperUser
from common.utils import get_logger
from .. import serializers
from ..models import Setting
logger = get_logger(__file__)
class MailTestingAPI(APIView):
permission_classes = (IsSuperUser,)
serializer_class = serializers.MailTestSerializer
success_message = _("Test mail sent to {}, please check")
def post(self, request):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
email_host = serializer.validated_data['EMAIL_HOST']
email_port = serializer.validated_data['EMAIL_PORT']
email_host_user = serializer.validated_data["EMAIL_HOST_USER"]
email_host_password = serializer.validated_data['EMAIL_HOST_PASSWORD']
email_from = serializer.validated_data["EMAIL_FROM"]
email_recipient = serializer.validated_data["EMAIL_RECIPIENT"]
email_use_ssl = serializer.validated_data['EMAIL_USE_SSL']
email_use_tls = serializer.validated_data['EMAIL_USE_TLS']
# 设置 settings 的值,会导致动态配置在当前进程失效
# for k, v in serializer.validated_data.items():
# if k.startswith('EMAIL'):
# setattr(settings, k, v)
try:
subject = "Test"
message = "Test smtp setting"
email_from = email_from or email_host_user
email_recipient = email_recipient or email_from
connection = get_connection(
host=email_host, port=email_port,
username=email_host_user, password=email_host_password,
use_tls=email_use_tls, use_ssl=email_use_ssl,
)
send_mail(
subject, message, email_from, [email_recipient],
connection=connection
)
except SMTPSenderRefused as e:
error = e.smtp_error
if isinstance(error, bytes):
for coding in ('gbk', 'utf8'):
try:
error = error.decode(coding)
except UnicodeDecodeError:
continue
else:
break
return Response({"error": str(error)}, status=400)
except Exception as e:
logger.error(e)
return Response({"error": str(e)}, status=400)
return Response({"msg": self.success_message.format(email_recipient)})
class PublicSettingApi(generics.RetrieveAPIView):
permission_classes = (AllowAny,)
serializer_class = serializers.PublicSettingSerializer
@staticmethod
def get_logo_urls():
logo_urls = {
'logo_logout': static('img/logo.png'),
'logo_index': static('img/logo_text.png'),
'login_image': static('img/login_image.jpg'),
'favicon': static('img/facio.ico')
}
if not settings.XPACK_ENABLED:
return logo_urls
from xpack.plugins.interface.models import Interface
obj = Interface.interface()
if not obj:
return logo_urls
for attr in ['logo_logout', 'logo_index', 'login_image', 'favicon']:
if getattr(obj, attr, '') and getattr(obj, attr).url:
logo_urls.update({attr: getattr(obj, attr).url})
return logo_urls
@staticmethod
def get_login_title():
default_title = _('Welcome to the JumpServer open source Bastion Host')
if not settings.XPACK_ENABLED:
return default_title
from xpack.plugins.interface.models import Interface
return Interface.get_login_title()
def get_object(self):
instance = {
"data": {
"WINDOWS_SKIP_ALL_MANUAL_PASSWORD": settings.WINDOWS_SKIP_ALL_MANUAL_PASSWORD,
"SECURITY_MAX_IDLE_TIME": settings.SECURITY_MAX_IDLE_TIME,
"XPACK_ENABLED": settings.XPACK_ENABLED,
"LOGIN_CONFIRM_ENABLE": settings.LOGIN_CONFIRM_ENABLE,
"SECURITY_VIEW_AUTH_NEED_MFA": settings.SECURITY_VIEW_AUTH_NEED_MFA,
"SECURITY_MFA_VERIFY_TTL": settings.SECURITY_MFA_VERIFY_TTL,
"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(),
"TICKETS_ENABLED": settings.TICKETS_ENABLED,
"PASSWORD_RULE": {
'SECURITY_PASSWORD_MIN_LENGTH': settings.SECURITY_PASSWORD_MIN_LENGTH,
'SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH': settings.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH,
'SECURITY_PASSWORD_UPPER_CASE': settings.SECURITY_PASSWORD_UPPER_CASE,
'SECURITY_PASSWORD_LOWER_CASE': settings.SECURITY_PASSWORD_LOWER_CASE,
'SECURITY_PASSWORD_NUMBER': settings.SECURITY_PASSWORD_NUMBER,
'SECURITY_PASSWORD_SPECIAL_CHAR': settings.SECURITY_PASSWORD_SPECIAL_CHAR,
},
"AUTH_WECOM": settings.AUTH_WECOM,
"AUTH_DINGTALK": settings.AUTH_DINGTALK,
"AUTH_FEISHU": settings.AUTH_FEISHU,
'SECURITY_WATERMARK_ENABLED': settings.SECURITY_WATERMARK_ENABLED
}
}
return instance
class SettingsApi(generics.RetrieveUpdateAPIView):
permission_classes = (IsSuperUser,)
serializer_class_mapper = {
'all': serializers.SettingsSerializer,
'basic': serializers.BasicSettingSerializer,
'terminal': serializers.TerminalSettingSerializer,
'security': serializers.SecuritySettingSerializer,
'ldap': serializers.LDAPSettingSerializer,
'email': serializers.EmailSettingSerializer,
'email_content': serializers.EmailContentSettingSerializer,
'wecom': serializers.WeComSettingSerializer,
'dingtalk': serializers.DingTalkSettingSerializer,
'feishu': serializers.FeiShuSettingSerializer,
}
def get_serializer_class(self):
category = self.request.query_params.get('category', serializers.BasicSettingSerializer)
return self.serializer_class_mapper.get(category, serializers.BasicSettingSerializer)
def get_fields(self):
serializer = self.get_serializer_class()()
fields = serializer.get_fields()
return fields
def get_object(self):
items = self.get_fields().keys()
obj = {item: getattr(settings, item) for item in items}
return obj
def parse_serializer_data(self, serializer):
data = []
fields = self.get_fields()
encrypted_items = [name for name, field in fields.items() if field.write_only]
category = self.request.query_params.get('category', '')
for name, value in serializer.validated_data.items():
encrypted = name in encrypted_items
if encrypted and value in ['', None]:
continue
data.append({
'name': name, 'value': value,
'encrypted': encrypted, 'category': category
})
return data
def perform_update(self, serializer):
settings_items = self.parse_serializer_data(serializer)
serializer_data = getattr(serializer, 'data', {})
for item in settings_items:
changed, setting = Setting.update_or_create(**item)
if not changed:
continue
serializer_data[setting.name] = setting.cleaned_value
setattr(serializer, '_data', serializer_data)

View File

@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
#
from smtplib import SMTPSenderRefused
from rest_framework.views import Response, APIView
from django.core.mail import send_mail, get_connection
from django.utils.translation import ugettext_lazy as _
from common.permissions import IsSuperUser
from common.utils import get_logger
from .. import serializers
logger = get_logger(__file__)
__all__ = ['MailTestingAPI']
class MailTestingAPI(APIView):
permission_classes = (IsSuperUser,)
serializer_class = serializers.MailTestSerializer
success_message = _("Test mail sent to {}, please check")
def post(self, request):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
email_host = serializer.validated_data['EMAIL_HOST']
email_port = serializer.validated_data['EMAIL_PORT']
email_host_user = serializer.validated_data["EMAIL_HOST_USER"]
email_host_password = serializer.validated_data['EMAIL_HOST_PASSWORD']
email_from = serializer.validated_data["EMAIL_FROM"]
email_recipient = serializer.validated_data["EMAIL_RECIPIENT"]
email_use_ssl = serializer.validated_data['EMAIL_USE_SSL']
email_use_tls = serializer.validated_data['EMAIL_USE_TLS']
# 设置 settings 的值,会导致动态配置在当前进程失效
# for k, v in serializer.validated_data.items():
# if k.startswith('EMAIL'):
# setattr(settings, k, v)
try:
subject = "Test"
message = "Test smtp setting"
email_from = email_from or email_host_user
email_recipient = email_recipient or email_from
connection = get_connection(
host=email_host, port=email_port,
username=email_host_user, password=email_host_password,
use_tls=email_use_tls, use_ssl=email_use_ssl,
)
send_mail(
subject, message, email_from, [email_recipient],
connection=connection
)
except SMTPSenderRefused as e:
error = e.smtp_error
if isinstance(error, bytes):
for coding in ('gbk', 'utf8'):
try:
error = error.decode(coding)
except UnicodeDecodeError:
continue
else:
break
return Response({"error": str(error)}, status=400)
except Exception as e:
logger.error(e)
return Response({"error": str(e)}, status=400)
return Response({"msg": self.success_message.format(email_recipient)})

View File

@ -0,0 +1,80 @@
from rest_framework import generics
from rest_framework.permissions import AllowAny
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.templatetags.static import static
from jumpserver.utils import has_valid_xpack_license
from common.utils import get_logger
from .. import serializers
logger = get_logger(__name__)
__all__ = ['PublicSettingApi']
class PublicSettingApi(generics.RetrieveAPIView):
permission_classes = (AllowAny,)
serializer_class = serializers.PublicSettingSerializer
@staticmethod
def get_logo_urls():
logo_urls = {
'logo_logout': static('img/logo.png'),
'logo_index': static('img/logo_text.png'),
'login_image': static('img/login_image.jpg'),
'favicon': static('img/facio.ico')
}
if not settings.XPACK_ENABLED:
return logo_urls
from xpack.plugins.interface.models import Interface
obj = Interface.interface()
if not obj:
return logo_urls
for attr in ['logo_logout', 'logo_index', 'login_image', 'favicon']:
if getattr(obj, attr, '') and getattr(obj, attr).url:
logo_urls.update({attr: getattr(obj, attr).url})
return logo_urls
@staticmethod
def get_login_title():
default_title = _('Welcome to the JumpServer open source Bastion Host')
if not settings.XPACK_ENABLED:
return default_title
from xpack.plugins.interface.models import Interface
return Interface.get_login_title()
def get_object(self):
instance = {
"data": {
"WINDOWS_SKIP_ALL_MANUAL_PASSWORD": settings.WINDOWS_SKIP_ALL_MANUAL_PASSWORD,
"SECURITY_MAX_IDLE_TIME": settings.SECURITY_MAX_IDLE_TIME,
"XPACK_ENABLED": settings.XPACK_ENABLED,
"LOGIN_CONFIRM_ENABLE": settings.LOGIN_CONFIRM_ENABLE,
"SECURITY_VIEW_AUTH_NEED_MFA": settings.SECURITY_VIEW_AUTH_NEED_MFA,
"SECURITY_MFA_VERIFY_TTL": settings.SECURITY_MFA_VERIFY_TTL,
"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(),
"TICKETS_ENABLED": settings.TICKETS_ENABLED,
"PASSWORD_RULE": {
'SECURITY_PASSWORD_MIN_LENGTH': settings.SECURITY_PASSWORD_MIN_LENGTH,
'SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH': settings.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH,
'SECURITY_PASSWORD_UPPER_CASE': settings.SECURITY_PASSWORD_UPPER_CASE,
'SECURITY_PASSWORD_LOWER_CASE': settings.SECURITY_PASSWORD_LOWER_CASE,
'SECURITY_PASSWORD_NUMBER': settings.SECURITY_PASSWORD_NUMBER,
'SECURITY_PASSWORD_SPECIAL_CHAR': settings.SECURITY_PASSWORD_SPECIAL_CHAR,
},
"AUTH_WECOM": settings.AUTH_WECOM,
"AUTH_DINGTALK": settings.AUTH_DINGTALK,
"AUTH_FEISHU": settings.AUTH_FEISHU,
'SECURITY_WATERMARK_ENABLED': settings.SECURITY_WATERMARK_ENABLED,
'SECURITY_SESSION_SHARE': settings.SECURITY_SESSION_SHARE,
"XRDP_ENABLED": settings.XRDP_ENABLED,
}
}
return instance

View File

@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
#
from rest_framework import generics
from django.conf import settings
from jumpserver.conf import Config
from common.permissions import IsSuperUser
from common.utils import get_logger
from .. import serializers
from ..models import Setting
logger = get_logger(__file__)
class SettingsApi(generics.RetrieveUpdateAPIView):
permission_classes = (IsSuperUser,)
serializer_class_mapper = {
'all': serializers.SettingsSerializer,
'basic': serializers.BasicSettingSerializer,
'terminal': serializers.TerminalSettingSerializer,
'security': serializers.SecuritySettingSerializer,
'ldap': serializers.LDAPSettingSerializer,
'email': serializers.EmailSettingSerializer,
'email_content': serializers.EmailContentSettingSerializer,
'wecom': serializers.WeComSettingSerializer,
'dingtalk': serializers.DingTalkSettingSerializer,
'feishu': serializers.FeiShuSettingSerializer,
'auth': serializers.AuthSettingSerializer,
'oidc': serializers.OIDCSettingSerializer,
'keycloak': serializers.KeycloakSettingSerializer,
'radius': serializers.RadiusSettingSerializer,
'cas': serializers.CASSettingSerializer,
'sso': serializers.SSOSettingSerializer,
'clean': serializers.CleaningSerializer,
'other': serializers.OtherSettingSerializer,
'alibaba': serializers.AlibabaSMSSettingSerializer,
'tencent': serializers.TencentSMSSettingSerializer,
}
def get_serializer_class(self):
category = self.request.query_params.get('category', 'basic')
default = serializers.BasicSettingSerializer
cls = self.serializer_class_mapper.get(category, default)
return cls
def get_fields(self):
serializer = self.get_serializer_class()()
fields = serializer.get_fields()
return fields
def get_object(self):
items = self.get_fields().keys()
obj = {}
for item in items:
if hasattr(settings, item):
obj[item] = getattr(settings, item)
else:
obj[item] = Config.defaults[item]
return obj
def parse_serializer_data(self, serializer):
data = []
fields = self.get_fields()
encrypted_items = [name for name, field in fields.items() if field.write_only]
category = self.request.query_params.get('category', '')
for name, value in serializer.validated_data.items():
encrypted = name in encrypted_items
if encrypted and value in ['', None]:
continue
data.append({
'name': name, 'value': value,
'encrypted': encrypted, 'category': category
})
return data
def perform_update(self, serializer):
settings_items = self.parse_serializer_data(serializer)
serializer_data = getattr(serializer, 'data', {})
for item in settings_items:
changed, setting = Setting.update_or_create(**item)
if not changed:
continue
serializer_data[setting.name] = setting.cleaned_value
setattr(serializer, '_data', serializer_data)
if hasattr(serializer, 'post_save'):
serializer.post_save()

22
apps/settings/api/sms.py Normal file
View File

@ -0,0 +1,22 @@
from rest_framework.generics import ListAPIView
from rest_framework.response import Response
from common.permissions import IsSuperUser
from common.message.backends.sms import BACKENDS
from settings.serializers.sms import SMSBackendSerializer
class SMSBackendAPI(ListAPIView):
permission_classes = (IsSuperUser,)
serializer_class = SMSBackendSerializer
def list(self, request, *args, **kwargs):
data = [
{
'name': b,
'label': b.label
}
for b in BACKENDS
]
return Response(data)

View File

@ -0,0 +1,63 @@
from collections import OrderedDict
from rest_framework.views import Response
from rest_framework.generics import GenericAPIView
from rest_framework.exceptions import APIException
from rest_framework import status
from django.utils.translation import gettext_lazy as _
from common.message.backends.sms import SMS_MESSAGE
from common.message.backends.sms.tencent import TencentSMS
from settings.models import Setting
from common.permissions import IsSuperUser
from common.exceptions import JMSException
from .. import serializers
class TencentSMSTestingAPI(GenericAPIView):
permission_classes = (IsSuperUser,)
serializer_class = serializers.TencentSMSSettingSerializer
def post(self, request):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
tencent_secret_id = serializer.validated_data['TENCENT_SECRET_ID']
tencent_secret_key = serializer.validated_data.get('TENCENT_SECRET_KEY')
tencent_sms_sign_and_tmpl = serializer.validated_data['TENCENT_SMS_SIGN_AND_TEMPLATES']
tencent_sdkappid = serializer.validated_data.get('TENCENT_SDKAPPID')
test_phone = serializer.validated_data.get('SMS_TEST_PHONE')
if not test_phone:
raise JMSException(code='test_phone_required', detail=_('test_phone is required'))
if not tencent_secret_key:
secret = Setting.objects.filter(name='TENCENT_SECRET_KEY').first()
if secret:
tencent_secret_key = secret.cleaned_value
tencent_secret_key = tencent_secret_key or ''
try:
client = TencentSMS(
secret_id=tencent_secret_id,
secret_key=tencent_secret_key,
sdkappid=tencent_sdkappid
)
sign, tmpl = SMS_MESSAGE.VERIFICATION_CODE.get_sign_and_tmpl(tencent_sms_sign_and_tmpl)
client.send_sms(
phone_numbers=[test_phone],
sign_name=sign,
template_code=tmpl,
template_param=OrderedDict(code='test')
)
return Response(status=status.HTTP_200_OK, data={'msg': _('Test success')})
except APIException as e:
try:
error = e.detail['errmsg']
except:
error = e.detail
return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': error})

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.12 on 2021-09-01 02:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('settings', '0002_auto_20210729_1546'),
]
operations = [
migrations.AlterField(
model_name='setting',
name='value',
field=models.TextField(blank=True, null=True, verbose_name='Value'),
),
]

View File

@ -27,7 +27,7 @@ class SettingManager(models.Manager):
class Setting(models.Model): class Setting(models.Model):
name = models.CharField(max_length=128, unique=True, verbose_name=_("Name")) name = models.CharField(max_length=128, unique=True, verbose_name=_("Name"))
value = models.TextField(verbose_name=_("Value")) value = models.TextField(verbose_name=_("Value"), null=True, blank=True)
category = models.CharField(max_length=128, default="default") category = models.CharField(max_length=128, default="default")
encrypted = models.BooleanField(default=False) encrypted = models.BooleanField(default=False)
enabled = models.BooleanField(verbose_name=_("Enabled"), default=True) enabled = models.BooleanField(verbose_name=_("Enabled"), default=True)

View File

@ -1,7 +1,13 @@
# coding: utf-8 # coding: utf-8
# #
from .basic import *
from .auth import *
from .email import * from .email import *
from .ldap import *
from .public import * from .public import *
from .settings import * from .settings import *
from .security import *
from .terminal import *
from .cleaning import *
from .other import *

View File

@ -0,0 +1,10 @@
from .cas import *
from .ldap import *
from .oidc import *
from .radius import *
from .dingtalk import *
from .feishu import *
from .wecom import *
from .sso import *
from .base import *
from .sms import *

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