mirror of https://github.com/jumpserver/jumpserver
commit
e993e7257c
11
Dockerfile
11
Dockerfile
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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']
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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)
|
||||||
|
]
|
|
@ -1 +1,2 @@
|
||||||
from .application import *
|
from .application import *
|
||||||
|
from .account import *
|
||||||
|
|
|
@ -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
|
|
@ -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},
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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():
|
||||||
|
|
|
@ -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,6 +46,10 @@ 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)
|
||||||
|
try:
|
||||||
|
s.save()
|
||||||
|
except IntegrityError:
|
||||||
|
s.id = None
|
||||||
s.save()
|
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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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']
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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下的资产
|
||||||
关联到上面
|
关联到上面
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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':
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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})
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
from .backends import *
|
from .backends import *
|
||||||
from .callback import *
|
|
||||||
|
|
|
@ -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()
|
|
|
@ -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:
|
||||||
|
self.msg = otp_failed_msg.format(
|
||||||
times_try=times_remainder, block_time=block_time
|
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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
|
@ -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'] = ''
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
|
@ -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 %}
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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__)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
# 只处理可预知的错误
|
# 只处理可预知的错误
|
||||||
|
|
|
@ -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))
|
|
@ -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
|
|
@ -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
|
|
@ -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']
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
]
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
try:
|
||||||
msg = get_msg_method()
|
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)
|
||||||
|
|
|
@ -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',)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 = [
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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'):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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']
|
||||||
|
|
||||||
|
|
|
@ -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'],
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
from django.db.models import TextChoices
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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)()
|
||||||
|
|
||||||
|
|
|
@ -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')},
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
被授权应用的数据结构
|
被授权应用的数据结构
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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')},
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
from . import common
|
from . import asset_permission
|
||||||
|
from . import app_permission
|
||||||
from . import refresh_perms
|
from . import refresh_perms
|
||||||
|
|
|
@ -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)
|
|
@ -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)
|
|
|
@ -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 *
|
||||||
|
|
|
@ -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})
|
|
@ -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)
|
|
|
@ -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)})
|
|
@ -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
|
|
@ -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()
|
|
@ -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)
|
|
@ -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})
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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)
|
||||||
|
|
|
@ -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 *
|
||||||
|
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue