mirror of https://github.com/jumpserver/jumpserver
commit
076adec218
|
@ -0,0 +1,2 @@
|
|||
*.mmdb filter=lfs diff=lfs merge=lfs -text
|
||||
*.mo filter=lfs diff=lfs merge=lfs -text
|
28
Dockerfile
28
Dockerfile
|
@ -1,4 +1,3 @@
|
|||
# 编译代码
|
||||
FROM python:3.8.6-slim as stage-build
|
||||
MAINTAINER JumpServer Team <ibuler@qq.com>
|
||||
ARG VERSION
|
||||
|
@ -8,22 +7,18 @@ WORKDIR /opt/jumpserver
|
|||
ADD . .
|
||||
RUN cd utils && bash -ixeu build.sh
|
||||
|
||||
|
||||
# 构建运行时环境
|
||||
FROM python:3.8.6-slim
|
||||
ARG PIP_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_MIRROR=$PIP_MIRROR
|
||||
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
COPY ./requirements/deb_buster_requirements.txt ./requirements/deb_buster_requirements.txt
|
||||
COPY ./requirements/deb_requirements.txt ./requirements/deb_requirements.txt
|
||||
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 \
|
||||
&& apt update \
|
||||
&& apt -y install telnet iproute2 redis-tools \
|
||||
&& grep -v '^#' ./requirements/deb_buster_requirements.txt | xargs apt -y install \
|
||||
&& apt -y install telnet iproute2 redis-tools default-mysql-client vim wget curl locales procps \
|
||||
&& apt -y install $(cat requirements/deb_requirements.txt) \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& localedef -c -f UTF-8 -i zh_CN zh_CN.UTF-8 \
|
||||
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
|
@ -32,21 +27,22 @@ RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
|
|||
|
||||
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} \
|
||||
&& pip config set global.index-url ${PIP_MIRROR} \
|
||||
&& pip install --no-cache-dir $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
|
||||
&& pip install --no-cache-dir -r requirements/requirements.txt
|
||||
&& pip install --no-cache-dir -r requirements/requirements.txt -i ${PIP_MIRROR} \
|
||||
&& rm -rf ~/.cache/pip
|
||||
|
||||
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||
RUN mkdir -p /root/.ssh/ \
|
||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config
|
||||
|
||||
RUN mkdir -p /opt/jumpserver/oracle/
|
||||
ADD https://download.jumpserver.org/public/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 mkdir -p /opt/jumpserver/oracle/ \
|
||||
&& wget https://download.jumpserver.org/public/instantclient-basiclite-linux.x64-21.1.0.0.0.tar > /dev/null \
|
||||
&& tar xf instantclient-basiclite-linux.x64-21.1.0.0.0.tar -C /opt/jumpserver/oracle/ \
|
||||
&& echo "/opt/jumpserver/oracle/instantclient_21_1" > /etc/ld.so.conf.d/oracle-instantclient.conf \
|
||||
&& ldconfig \
|
||||
&& rm -f instantclient-basiclite-linux.x64-21.1.0.0.0.tar
|
||||
|
||||
RUN echo > config.yml
|
||||
|
||||
VOLUME /opt/jumpserver/data
|
||||
VOLUME /opt/jumpserver/logs
|
||||
|
||||
|
|
|
@ -2,15 +2,16 @@ from common.permissions import IsOrgAdmin, HasQueryParamsUserAndIsCurrentOrgMemb
|
|||
from common.drf.api import JMSBulkModelViewSet
|
||||
from ..models import LoginACL
|
||||
from .. import serializers
|
||||
from ..filters import LoginAclFilter
|
||||
|
||||
__all__ = ['LoginACLViewSet', ]
|
||||
|
||||
|
||||
class LoginACLViewSet(JMSBulkModelViewSet):
|
||||
queryset = LoginACL.objects.all()
|
||||
filterset_fields = ('name', 'user', )
|
||||
search_fields = filterset_fields
|
||||
permission_classes = (IsOrgAdmin, )
|
||||
filterset_class = LoginAclFilter
|
||||
search_fields = ('name',)
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.LoginACLSerializer
|
||||
|
||||
def get_permissions(self):
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from common.permissions import IsOrgAdmin
|
||||
from .. import models, serializers
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.generics import CreateAPIView, RetrieveDestroyAPIView
|
||||
from rest_framework.generics import CreateAPIView
|
||||
|
||||
from common.permissions import IsAppUser
|
||||
from common.utils import reverse, lazyproperty
|
||||
from orgs.utils import tmp_to_org, tmp_to_root_org
|
||||
from orgs.utils import tmp_to_org
|
||||
from tickets.api import GenericTicketStatusRetrieveCloseAPI
|
||||
from ..models import LoginAssetACL
|
||||
from .. import serializers
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
from django_filters import rest_framework as filters
|
||||
from common.drf.filters import BaseFilterSet
|
||||
|
||||
from acls.models import LoginACL
|
||||
|
||||
|
||||
class LoginAclFilter(BaseFilterSet):
|
||||
user = filters.UUIDFilter(field_name='user_id')
|
||||
user_display = filters.CharFilter(field_name='user__name')
|
||||
|
||||
class Meta:
|
||||
model = LoginACL
|
||||
fields = (
|
||||
'name', 'user', 'user_display'
|
||||
)
|
|
@ -0,0 +1,87 @@
|
|||
# Generated by Django 3.1.12 on 2021-09-26 02:47
|
||||
import django
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models, transaction
|
||||
from acls.models import LoginACL
|
||||
|
||||
LOGIN_CONFIRM_ZH = '登录复核'
|
||||
LOGIN_CONFIRM_EN = 'Login confirm'
|
||||
|
||||
|
||||
def has_zh(name: str) -> bool:
|
||||
for i in name:
|
||||
if u'\u4e00' <= i <= u'\u9fff':
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def migrate_login_confirm(apps, schema_editor):
|
||||
login_acl_model = apps.get_model("acls", "LoginACL")
|
||||
login_confirm_model = apps.get_model("authentication", "LoginConfirmSetting")
|
||||
|
||||
with transaction.atomic():
|
||||
for instance in login_confirm_model.objects.filter(is_active=True):
|
||||
user = instance.user
|
||||
reviewers = instance.reviewers.all()
|
||||
login_confirm = LOGIN_CONFIRM_ZH if has_zh(user.name) else LOGIN_CONFIRM_EN
|
||||
date_created = instance.date_created.strftime('%Y-%m-%d %H:%M:%S')
|
||||
if reviewers.count() == 0:
|
||||
continue
|
||||
data = {
|
||||
'user': user,
|
||||
'name': f'{user.name}-{login_confirm} ({date_created})',
|
||||
'created_by': instance.created_by,
|
||||
'action': LoginACL.ActionChoices.confirm
|
||||
}
|
||||
instance = login_acl_model.objects.create(**data)
|
||||
instance.reviewers.set(reviewers)
|
||||
|
||||
|
||||
def migrate_ip_group(apps, schema_editor):
|
||||
login_acl_model = apps.get_model("acls", "LoginACL")
|
||||
default_time_periods = [{'id': i, 'value': '00:00~00:00'} for i in range(7)]
|
||||
updates = list()
|
||||
with transaction.atomic():
|
||||
for instance in login_acl_model.objects.exclude(action=LoginACL.ActionChoices.confirm):
|
||||
instance.rules = {'ip_group': instance.ip_group, 'time_period': default_time_periods}
|
||||
updates.append(instance)
|
||||
login_acl_model.objects.bulk_update(updates, ['rules', ])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('acls', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='loginacl',
|
||||
name='action',
|
||||
field=models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow'), ('confirm', 'Login confirm')],
|
||||
default='reject', max_length=64, verbose_name='Action'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='loginacl',
|
||||
name='reviewers',
|
||||
field=models.ManyToManyField(blank=True, related_name='login_confirm_acls',
|
||||
to=settings.AUTH_USER_MODEL, verbose_name='Reviewers'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='loginacl',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='login_acls', to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
||||
),
|
||||
migrations.RunPython(migrate_login_confirm),
|
||||
migrations.AddField(
|
||||
model_name='loginacl',
|
||||
name='rules',
|
||||
field=models.JSONField(default=dict, verbose_name='Rule'),
|
||||
),
|
||||
migrations.RunPython(migrate_ip_group),
|
||||
migrations.RemoveField(
|
||||
model_name='loginacl',
|
||||
name='ip_group',
|
||||
),
|
||||
]
|
|
@ -1,8 +1,11 @@
|
|||
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from .base import BaseACL, BaseACLQuerySet
|
||||
from common.utils import get_request_ip, get_ip_city
|
||||
from common.utils.ip import contains_ip
|
||||
from common.utils.time_period import contains_time_period
|
||||
|
||||
|
||||
class ACLManager(models.Manager):
|
||||
|
@ -15,19 +18,24 @@ class LoginACL(BaseACL):
|
|||
class ActionChoices(models.TextChoices):
|
||||
reject = 'reject', _('Reject')
|
||||
allow = 'allow', _('Allow')
|
||||
confirm = 'confirm', _('Login confirm')
|
||||
|
||||
# 条件
|
||||
ip_group = models.JSONField(default=list, verbose_name=_('Login IP'))
|
||||
# 用户
|
||||
user = models.ForeignKey(
|
||||
'users.User', on_delete=models.CASCADE, verbose_name=_('User'),
|
||||
related_name='login_acls'
|
||||
)
|
||||
# 规则
|
||||
rules = models.JSONField(default=dict, verbose_name=_('Rule'))
|
||||
# 动作
|
||||
action = models.CharField(
|
||||
max_length=64, choices=ActionChoices.choices, default=ActionChoices.reject,
|
||||
verbose_name=_('Action')
|
||||
max_length=64, verbose_name=_('Action'),
|
||||
choices=ActionChoices.choices, default=ActionChoices.reject
|
||||
)
|
||||
# 关联
|
||||
user = models.ForeignKey(
|
||||
'users.User', on_delete=models.CASCADE, related_name='login_acls', verbose_name=_('User')
|
||||
reviewers = models.ManyToManyField(
|
||||
'users.User', verbose_name=_("Reviewers"),
|
||||
related_name="login_confirm_acls", blank=True
|
||||
)
|
||||
|
||||
objects = ACLManager.from_queryset(BaseACLQuerySet)()
|
||||
|
||||
class Meta:
|
||||
|
@ -44,14 +52,73 @@ class LoginACL(BaseACL):
|
|||
def action_allow(self):
|
||||
return self.action == self.ActionChoices.allow
|
||||
|
||||
@classmethod
|
||||
def filter_acl(cls, user):
|
||||
return user.login_acls.all().valid().distinct()
|
||||
|
||||
@staticmethod
|
||||
def allow_user_confirm_if_need(user, ip):
|
||||
acl = LoginACL.filter_acl(user).filter(action=LoginACL.ActionChoices.confirm).first()
|
||||
acl = acl if acl and acl.reviewers.exists() else None
|
||||
if not acl:
|
||||
return False, acl
|
||||
ip_group = acl.rules.get('ip_group')
|
||||
time_periods = acl.rules.get('time_period')
|
||||
is_contain_ip = contains_ip(ip, ip_group)
|
||||
is_contain_time_period = contains_time_period(time_periods)
|
||||
return is_contain_ip and is_contain_time_period, acl
|
||||
|
||||
@staticmethod
|
||||
def allow_user_to_login(user, ip):
|
||||
acl = user.login_acls.valid().first()
|
||||
acl = LoginACL.filter_acl(user).exclude(action=LoginACL.ActionChoices.confirm).first()
|
||||
if not acl:
|
||||
return True
|
||||
is_contained = contains_ip(ip, acl.ip_group)
|
||||
if acl.action_allow and is_contained:
|
||||
return True
|
||||
if acl.action_reject and not is_contained:
|
||||
return True
|
||||
return False
|
||||
return True, ''
|
||||
ip_group = acl.rules.get('ip_group')
|
||||
time_periods = acl.rules.get('time_period')
|
||||
is_contain_ip = contains_ip(ip, ip_group)
|
||||
is_contain_time_period = contains_time_period(time_periods)
|
||||
|
||||
reject_type = ''
|
||||
if is_contain_ip and is_contain_time_period:
|
||||
# 满足条件
|
||||
allow = acl.action_allow
|
||||
if not allow:
|
||||
reject_type = 'ip' if is_contain_ip else 'time'
|
||||
else:
|
||||
# 不满足条件
|
||||
# 如果acl本身允许,那就拒绝;如果本身拒绝,那就允许
|
||||
allow = not acl.action_allow
|
||||
if not allow:
|
||||
reject_type = 'ip' if not is_contain_ip else 'time'
|
||||
|
||||
return allow, reject_type
|
||||
|
||||
@staticmethod
|
||||
def construct_confirm_ticket_meta(request=None):
|
||||
login_ip = get_request_ip(request) if request else ''
|
||||
login_ip = login_ip or '0.0.0.0'
|
||||
login_city = get_ip_city(login_ip)
|
||||
login_datetime = timezone.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
ticket_meta = {
|
||||
'apply_login_ip': login_ip,
|
||||
'apply_login_city': login_city,
|
||||
'apply_login_datetime': login_datetime,
|
||||
}
|
||||
return ticket_meta
|
||||
|
||||
def create_confirm_ticket(self, request=None):
|
||||
from tickets import const
|
||||
from tickets.models import Ticket
|
||||
from orgs.models import Organization
|
||||
ticket_title = _('Login confirm') + ' {}'.format(self.user)
|
||||
ticket_meta = self.construct_confirm_ticket_meta(request)
|
||||
data = {
|
||||
'title': ticket_title,
|
||||
'type': const.TicketType.login_confirm.value,
|
||||
'meta': ticket_meta,
|
||||
'org_id': Organization.ROOT_ID,
|
||||
}
|
||||
ticket = Ticket.objects.create(**data)
|
||||
ticket.create_process_map_and_node(self.reviewers.all())
|
||||
ticket.open(self.user)
|
||||
return ticket
|
||||
|
|
|
@ -1,59 +1,42 @@
|
|||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import serializers
|
||||
from common.drf.serializers import BulkModelSerializer
|
||||
from orgs.utils import current_org
|
||||
from common.drf.serializers import MethodSerializer
|
||||
from ..models import LoginACL
|
||||
from common.utils.ip import is_ip_address, is_ip_network, is_ip_segment
|
||||
|
||||
from .rules import RuleSerializer
|
||||
|
||||
__all__ = ['LoginACLSerializer', ]
|
||||
|
||||
|
||||
def ip_group_child_validator(ip_group_child):
|
||||
is_valid = ip_group_child == '*' \
|
||||
or is_ip_address(ip_group_child) \
|
||||
or is_ip_network(ip_group_child) \
|
||||
or is_ip_segment(ip_group_child)
|
||||
if not is_valid:
|
||||
error = _('IP address invalid: `{}`').format(ip_group_child)
|
||||
raise serializers.ValidationError(error)
|
||||
common_help_text = _('Format for comma-delimited string, with * indicating a match all. ')
|
||||
|
||||
|
||||
class LoginACLSerializer(BulkModelSerializer):
|
||||
ip_group_help_text = _(
|
||||
'Format for comma-delimited string, with * indicating a match all. '
|
||||
'Such as: '
|
||||
'192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 '
|
||||
)
|
||||
|
||||
ip_group = serializers.ListField(
|
||||
default=['*'], label=_('IP'), help_text=ip_group_help_text,
|
||||
child=serializers.CharField(max_length=1024, validators=[ip_group_child_validator])
|
||||
)
|
||||
user_display = serializers.ReadOnlyField(source='user.name', label=_('User'))
|
||||
user_display = serializers.ReadOnlyField(source='user.name', label=_('Username'))
|
||||
reviewers_display = serializers.SerializerMethodField(label=_('Reviewers'))
|
||||
action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action'))
|
||||
reviewers_amount = serializers.IntegerField(read_only=True, source='reviewers.count')
|
||||
rules = MethodSerializer()
|
||||
|
||||
class Meta:
|
||||
model = LoginACL
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'priority', 'ip_group', 'action', 'action_display',
|
||||
'is_active',
|
||||
'date_created', 'date_updated',
|
||||
'comment', 'created_by',
|
||||
'priority', 'rules', 'action', 'action_display',
|
||||
'is_active', 'user', 'user_display',
|
||||
'date_created', 'date_updated', 'reviewers_amount',
|
||||
'comment', 'created_by'
|
||||
]
|
||||
fields_fk = ['user', 'user_display',]
|
||||
fields = fields_small + fields_fk
|
||||
fields_fk = ['user', 'user_display']
|
||||
fields_m2m = ['reviewers', 'reviewers_display']
|
||||
fields = fields_small + fields_fk + fields_m2m
|
||||
extra_kwargs = {
|
||||
'priority': {'default': 50},
|
||||
'is_active': {'default': True},
|
||||
"reviewers": {'allow_null': False, 'required': True},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def validate_user(user):
|
||||
if user not in current_org.get_members():
|
||||
error = _('The user `{}` is not in the current organization: `{}`').format(
|
||||
user, current_org
|
||||
)
|
||||
raise serializers.ValidationError(error)
|
||||
return user
|
||||
def get_rules_serializer(self):
|
||||
return RuleSerializer()
|
||||
|
||||
def get_reviewers_display(self, obj):
|
||||
return ','.join([str(user) for user in obj.reviewers.all()])
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
from .rules import *
|
|
@ -0,0 +1,34 @@
|
|||
# coding: utf-8
|
||||
#
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.utils.ip import is_ip_address, is_ip_network, is_ip_segment
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
__all__ = ['RuleSerializer']
|
||||
|
||||
|
||||
def ip_group_child_validator(ip_group_child):
|
||||
is_valid = ip_group_child == '*' \
|
||||
or is_ip_address(ip_group_child) \
|
||||
or is_ip_network(ip_group_child) \
|
||||
or is_ip_segment(ip_group_child)
|
||||
if not is_valid:
|
||||
error = _('IP address invalid: `{}`').format(ip_group_child)
|
||||
raise serializers.ValidationError(error)
|
||||
|
||||
|
||||
class RuleSerializer(serializers.Serializer):
|
||||
ip_group_help_text = _(
|
||||
'Format for comma-delimited string, with * indicating a match all. '
|
||||
'Such as: '
|
||||
'192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 '
|
||||
)
|
||||
|
||||
ip_group = serializers.ListField(
|
||||
default=['*'], label=_('IP'), help_text=ip_group_help_text,
|
||||
child=serializers.CharField(max_length=1024, validators=[ip_group_child_validator]))
|
||||
time_period = serializers.ListField(default=[], label=_('Time Period'))
|
|
@ -32,6 +32,8 @@ class SerializeApplicationToTreeNodeMixin:
|
|||
return node
|
||||
|
||||
def serialize_applications_with_org(self, applications):
|
||||
if not applications:
|
||||
return []
|
||||
root_node = self.create_root_node()
|
||||
tree_nodes = [root_node]
|
||||
organizations = self.filter_organizations(applications)
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 3.1.13 on 2021-10-14 14:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applications', '0011_auto_20210826_1759'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='account',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='historicalaccount',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username'),
|
||||
),
|
||||
]
|
|
@ -2,12 +2,19 @@
|
|||
#
|
||||
from assets.api import FilterAssetByNodeMixin
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework.generics import RetrieveAPIView
|
||||
from rest_framework.generics import RetrieveAPIView, ListAPIView
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.db.models import Q
|
||||
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsSuperUser
|
||||
from common.mixins.views import SuggestionMixin
|
||||
from users.models import User, UserGroup
|
||||
from users.serializers import UserSerializer, UserGroupSerializer
|
||||
from users.filters import UserFilter
|
||||
from perms.models import AssetPermission
|
||||
from perms.serializers import AssetPermissionSerializer
|
||||
from perms.filters import AssetPermissionFilter
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.mixins import generics
|
||||
from ..models import Asset, Node, Platform
|
||||
|
@ -23,6 +30,8 @@ __all__ = [
|
|||
'AssetViewSet', 'AssetPlatformRetrieveApi',
|
||||
'AssetGatewayListApi', 'AssetPlatformViewSet',
|
||||
'AssetTaskCreateApi', 'AssetsTaskCreateApi',
|
||||
'AssetPermUserListApi', 'AssetPermUserPermissionsListApi',
|
||||
'AssetPermUserGroupListApi', 'AssetPermUserGroupPermissionsListApi',
|
||||
]
|
||||
|
||||
|
||||
|
@ -170,3 +179,102 @@ class AssetGatewayListApi(generics.ListAPIView):
|
|||
return []
|
||||
queryset = asset.domain.gateways.filter(protocol='ssh')
|
||||
return queryset
|
||||
|
||||
|
||||
class BaseAssetPermUserOrUserGroupListApi(ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def get_object(self):
|
||||
asset_id = self.kwargs.get('pk')
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
return asset
|
||||
|
||||
def get_asset_related_perms(self):
|
||||
asset = self.get_object()
|
||||
nodes = asset.get_all_nodes(flat=True)
|
||||
perms = AssetPermission.objects.filter(Q(assets=asset) | Q(nodes__in=nodes))
|
||||
return perms
|
||||
|
||||
|
||||
class AssetPermUserListApi(BaseAssetPermUserOrUserGroupListApi):
|
||||
filterset_class = UserFilter
|
||||
search_fields = ('username', 'email', 'name', 'id', 'source', 'role')
|
||||
serializer_class = UserSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
perms = self.get_asset_related_perms()
|
||||
users = User.objects.filter(
|
||||
Q(assetpermissions__in=perms) | Q(groups__assetpermissions__in=perms)
|
||||
).distinct()
|
||||
return users
|
||||
|
||||
|
||||
class AssetPermUserGroupListApi(BaseAssetPermUserOrUserGroupListApi):
|
||||
serializer_class = UserGroupSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
perms = self.get_asset_related_perms()
|
||||
user_groups = UserGroup.objects.filter(assetpermissions__in=perms).distinct()
|
||||
return user_groups
|
||||
|
||||
|
||||
class BaseAssetPermUserOrUserGroupPermissionsListApiMixin(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
model = AssetPermission
|
||||
serializer_class = AssetPermissionSerializer
|
||||
filterset_class = AssetPermissionFilter
|
||||
search_fields = ('name',)
|
||||
|
||||
def get_object(self):
|
||||
asset_id = self.kwargs.get('pk')
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
return asset
|
||||
|
||||
def filter_asset_related(self, queryset):
|
||||
asset = self.get_object()
|
||||
nodes = asset.get_all_nodes(flat=True)
|
||||
perms = queryset.filter(Q(assets=asset) | Q(nodes__in=nodes))
|
||||
return perms
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
queryset = super().filter_queryset(queryset)
|
||||
queryset = self.filter_asset_related(queryset)
|
||||
return queryset
|
||||
|
||||
|
||||
class AssetPermUserPermissionsListApi(BaseAssetPermUserOrUserGroupPermissionsListApiMixin):
|
||||
def filter_queryset(self, queryset):
|
||||
queryset = super().filter_queryset(queryset)
|
||||
queryset = self.filter_user_related(queryset)
|
||||
queryset = queryset.distinct()
|
||||
return queryset
|
||||
|
||||
def filter_user_related(self, queryset):
|
||||
user = self.get_perm_user()
|
||||
user_groups = user.groups.all()
|
||||
perms = queryset.filter(Q(users=user) | Q(user_groups__in=user_groups))
|
||||
return perms
|
||||
|
||||
def get_perm_user(self):
|
||||
user_id = self.kwargs.get('perm_user_id')
|
||||
user = get_object_or_404(User, pk=user_id)
|
||||
return user
|
||||
|
||||
|
||||
class AssetPermUserGroupPermissionsListApi(BaseAssetPermUserOrUserGroupPermissionsListApiMixin):
|
||||
def filter_queryset(self, queryset):
|
||||
queryset = super().filter_queryset(queryset)
|
||||
queryset = self.filter_user_group_related(queryset)
|
||||
queryset = queryset.distinct()
|
||||
return queryset
|
||||
|
||||
def filter_user_group_related(self, queryset):
|
||||
user_group = self.get_perm_user_group()
|
||||
perms = queryset.filter(user_groups=user_group)
|
||||
return perms
|
||||
|
||||
def get_perm_user_group(self):
|
||||
user_group_id = self.kwargs.get('perm_user_group_id')
|
||||
user_group = get_object_or_404(UserGroup, pk=user_group_id)
|
||||
return user_group
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@ from ..tasks import (
|
|||
push_system_user_to_assets
|
||||
)
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi',
|
||||
|
@ -91,7 +90,7 @@ class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView):
|
|||
asset_id = self.kwargs.get('asset_id')
|
||||
user_id = self.request.query_params.get("user_id")
|
||||
username = self.request.query_params.get("username")
|
||||
instance.load_asset_more_auth(asset_id=asset_id, user_id=user_id, username=username)
|
||||
instance.load_asset_more_auth(asset_id, username, user_id)
|
||||
return instance
|
||||
|
||||
|
||||
|
@ -107,8 +106,7 @@ class SystemUserAppAuthInfoApi(generics.RetrieveAPIView):
|
|||
instance = super().get_object()
|
||||
app_id = self.kwargs.get('app_id')
|
||||
user_id = self.request.query_params.get("user_id")
|
||||
if user_id:
|
||||
instance.load_app_more_auth(app_id, user_id)
|
||||
instance.load_app_more_auth(app_id, user_id)
|
||||
return instance
|
||||
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ def migrate_system_assets_to_authbook(apps, schema_editor):
|
|||
system_users = system_user_model.objects.all()
|
||||
for s in system_users:
|
||||
while True:
|
||||
systemuser_asset_relations = system_user_asset_model.objects.filter(systemuser=s)[:20]
|
||||
systemuser_asset_relations = system_user_asset_model.objects.filter(systemuser=s)[:1000]
|
||||
if not systemuser_asset_relations:
|
||||
break
|
||||
authbooks = []
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 3.1.12 on 2021-10-12 08:42
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_platform_win2016(apps, schema_editor):
|
||||
platform_model = apps.get_model("assets", "Platform")
|
||||
win2016 = platform_model.objects.filter(name='Windows2016').first()
|
||||
if not win2016:
|
||||
print("Error: Not found Windows2016 platform")
|
||||
return
|
||||
win2016.meta = {"security": "any"}
|
||||
win2016.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0076_delete_assetuser'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_platform_win2016)
|
||||
]
|
|
@ -0,0 +1,38 @@
|
|||
# Generated by Django 3.1.13 on 2021-10-14 14:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0077_auto_20211012_1642'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='authbook',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gateway',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='historicalauthbook',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username'),
|
||||
),
|
||||
]
|
|
@ -94,25 +94,27 @@ class AuthBook(BaseUser, AbsConnectivity):
|
|||
i.private_key = self.private_key
|
||||
i.public_key = self.public_key
|
||||
i.comment = 'Update triggered by account {}'.format(self.id)
|
||||
i.save(update_fields=['password', 'private_key', 'public_key'])
|
||||
|
||||
# 不触发post_save信号
|
||||
self.__class__.objects.bulk_update(matched, fields=['password', 'private_key', 'public_key'])
|
||||
|
||||
def remove_asset_admin_user_if_need(self):
|
||||
if not self.asset or not self.asset.admin_user:
|
||||
if not self.asset or not self.systemuser:
|
||||
return
|
||||
if not self.systemuser.is_admin_user:
|
||||
if not self.systemuser.is_admin_user or self.asset.admin_user != self.systemuser:
|
||||
return
|
||||
logger.debug('Remove asset admin user: {} {}'.format(self.asset, self.systemuser))
|
||||
self.asset.admin_user = None
|
||||
self.asset.save()
|
||||
logger.debug('Remove asset admin user: {} {}'.format(self.asset, self.systemuser))
|
||||
|
||||
def update_asset_admin_user_if_need(self):
|
||||
if not self.systemuser or not self.systemuser.is_admin_user:
|
||||
if not self.asset or not self.systemuser:
|
||||
return
|
||||
if not self.asset or self.asset.admin_user == self.systemuser:
|
||||
if not self.systemuser.is_admin_user or self.asset.admin_user == self.systemuser:
|
||||
return
|
||||
logger.debug('Update asset admin user: {} {}'.format(self.asset, self.systemuser))
|
||||
self.asset.admin_user = self.systemuser
|
||||
self.asset.save()
|
||||
logger.debug('Update asset admin user: {} {}'.format(self.asset, self.systemuser))
|
||||
|
||||
def __str__(self):
|
||||
return self.smart_name
|
||||
|
|
|
@ -173,7 +173,7 @@ class AuthMixin:
|
|||
class BaseUser(OrgModelMixin, AuthMixin):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), validators=[alphanumeric], db_index=True)
|
||||
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True)
|
||||
password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password'))
|
||||
private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key'))
|
||||
public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key'))
|
||||
|
|
|
@ -5,12 +5,14 @@ from django.core.validators import RegexValidator
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from users.models import User, UserGroup
|
||||
from perms.models import AssetPermission
|
||||
from ..models import Asset, Node, Platform, SystemUser
|
||||
|
||||
__all__ = [
|
||||
'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer',
|
||||
'ProtocolsField', 'PlatformSerializer',
|
||||
'AssetTaskSerializer', 'AssetsTaskSerializer', 'ProtocolsField'
|
||||
'AssetTaskSerializer', 'AssetsTaskSerializer', 'ProtocolsField',
|
||||
]
|
||||
|
||||
|
||||
|
@ -74,11 +76,11 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
|
|||
model = Asset
|
||||
fields_mini = ['id', 'hostname', 'ip', 'platform', 'protocols']
|
||||
fields_small = fields_mini + [
|
||||
'protocol', 'port', 'protocols', 'is_active', 'public_ip',
|
||||
'comment',
|
||||
'protocol', 'port', 'protocols', 'is_active',
|
||||
'public_ip', 'number', 'comment',
|
||||
]
|
||||
hardware_fields = [
|
||||
'number', 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count',
|
||||
'vendor', 'model', 'sn', 'cpu_model', 'cpu_count',
|
||||
'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info',
|
||||
'os', 'os_version', 'os_arch', 'hostname_raw', 'hardware_info',
|
||||
'connectivity', 'date_verified'
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.validators import alphanumeric
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from ..models import Domain, Gateway
|
||||
from .base import AuthSerializerMixin
|
||||
|
@ -59,6 +60,7 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||
fields_fk = ['domain']
|
||||
fields = fields_small + fields_fk
|
||||
extra_kwargs = {
|
||||
'username': {"validators": [alphanumeric]},
|
||||
'password': {'write_only': True},
|
||||
'private_key': {"write_only": True},
|
||||
'public_key': {"write_only": True},
|
||||
|
|
|
@ -4,6 +4,7 @@ from django.db.models import Count
|
|||
|
||||
from common.mixins.serializers import BulkSerializerMixin
|
||||
from common.utils import ssh_pubkey_gen
|
||||
from common.validators import alphanumeric_re, alphanumeric_cn_re
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from ..models import SystemUser, Asset
|
||||
from .utils import validate_password_contains_left_double_curly_bracket
|
||||
|
@ -97,15 +98,20 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||
raise serializers.ValidationError(error)
|
||||
|
||||
def validate_username(self, username):
|
||||
if username:
|
||||
return username
|
||||
login_mode = self.get_initial_value("login_mode")
|
||||
protocol = self.get_initial_value("protocol")
|
||||
username_same_with_user = self.get_initial_value("username_same_with_user")
|
||||
if username:
|
||||
regx = alphanumeric_re
|
||||
if protocol == SystemUser.Protocol.telnet:
|
||||
regx = alphanumeric_cn_re
|
||||
if not regx.match(username):
|
||||
raise serializers.ValidationError(_('Special char not allowed'))
|
||||
return username
|
||||
|
||||
username_same_with_user = self.get_initial_value("username_same_with_user")
|
||||
if username_same_with_user:
|
||||
return ''
|
||||
|
||||
login_mode = self.get_initial_value("login_mode")
|
||||
if login_mode == SystemUser.LOGIN_AUTO and protocol != SystemUser.Protocol.vnc:
|
||||
msg = _('* Automatic login mode must fill in the username.')
|
||||
raise serializers.ValidationError(msg)
|
||||
|
@ -181,7 +187,9 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
queryset = queryset.annotate(assets_amount=Count("assets"))
|
||||
queryset = queryset\
|
||||
.annotate(assets_amount=Count("assets"))\
|
||||
.prefetch_related('nodes', 'cmd_filters')
|
||||
return queryset
|
||||
|
||||
|
||||
|
|
|
@ -34,9 +34,11 @@ def on_authbook_post_delete(sender, instance, **kwargs):
|
|||
|
||||
|
||||
@receiver(post_save, sender=AuthBook)
|
||||
def on_authbook_post_create(sender, instance, **kwargs):
|
||||
def on_authbook_post_create(sender, instance, created, **kwargs):
|
||||
instance.sync_to_system_user_account()
|
||||
instance.update_asset_admin_user_if_need()
|
||||
if created:
|
||||
# 只在创建时进行更新资产的管理用户
|
||||
instance.update_asset_admin_user_if_need()
|
||||
|
||||
|
||||
@receiver(pre_save, sender=AuthBook)
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
from celery import shared_task
|
||||
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from assets.models import AuthBook
|
||||
|
||||
__all__ = ['add_nodes_assets_to_system_users']
|
||||
|
||||
|
@ -15,4 +16,12 @@ def add_nodes_assets_to_system_users(nodes_keys, system_users):
|
|||
nodes = Node.objects.filter(key__in=nodes_keys)
|
||||
assets = Node.get_nodes_all_assets(*nodes)
|
||||
for system_user in system_users:
|
||||
system_user.assets.add(*tuple(assets))
|
||||
""" 解决资产和节点进行关联时,已经关联过的节点不会触发 authbook post_save 信号,
|
||||
无法更新节点下所有资产的管理用户的问题 """
|
||||
for asset in assets:
|
||||
defaults = {'asset': asset, 'systemuser': system_user, 'org_id': asset.org_id}
|
||||
instance, created = AuthBook.objects.update_or_create(
|
||||
defaults=defaults, asset=asset, systemuser=system_user
|
||||
)
|
||||
# 只要关联都需要更新资产的管理用户
|
||||
instance.update_asset_admin_user_if_need()
|
||||
|
|
|
@ -36,6 +36,10 @@ urlpatterns = [
|
|||
path('assets/<uuid:pk>/platform/', api.AssetPlatformRetrieveApi.as_view(), name='asset-platform-detail'),
|
||||
path('assets/<uuid:pk>/tasks/', api.AssetTaskCreateApi.as_view(), name='asset-task-create'),
|
||||
path('assets/tasks/', api.AssetsTaskCreateApi.as_view(), name='assets-task-create'),
|
||||
path('assets/<uuid:pk>/perm-users/', api.AssetPermUserListApi.as_view(), name='asset-perm-user-list'),
|
||||
path('assets/<uuid:pk>/perm-users/<uuid:perm_user_id>/permissions/', api.AssetPermUserPermissionsListApi.as_view(), name='asset-perm-user-permission-list'),
|
||||
path('assets/<uuid:pk>/perm-user-groups/', api.AssetPermUserGroupListApi.as_view(), name='asset-perm-user-group-list'),
|
||||
path('assets/<uuid:pk>/perm-user-groups/<uuid:perm_user_group_id>/permissions/', api.AssetPermUserGroupPermissionsListApi.as_view(), name='asset-perm-user-group-permission-list'),
|
||||
|
||||
path('system-users/<uuid:pk>/auth-info/', api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'),
|
||||
path('system-users/<uuid:pk>/assets/', api.SystemUserAssetsListView.as_view(), name='system-user-assets'),
|
||||
|
|
|
@ -39,7 +39,7 @@ class UserLoginLogViewSet(ListModelMixin, CommonGenericViewSet):
|
|||
('datetime', ('date_from', 'date_to'))
|
||||
]
|
||||
filterset_fields = ['username', 'ip', 'city', 'type', 'status', 'mfa']
|
||||
search_fields =['username', 'ip', 'city']
|
||||
search_fields = ['username', 'ip', 'city']
|
||||
|
||||
@staticmethod
|
||||
def get_org_members():
|
||||
|
@ -48,9 +48,10 @@ class UserLoginLogViewSet(ListModelMixin, CommonGenericViewSet):
|
|||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
if not current_org.is_default():
|
||||
users = self.get_org_members()
|
||||
queryset = queryset.filter(username__in=users)
|
||||
if current_org.is_root():
|
||||
return queryset
|
||||
users = self.get_org_members()
|
||||
queryset = queryset.filter(username__in=users)
|
||||
return queryset
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
DEFAULT_CITY = _("Unknown")
|
|
@ -35,13 +35,14 @@ class UserLoginLogSerializer(serializers.ModelSerializer):
|
|||
fields_mini = ['id']
|
||||
fields_small = fields_mini + [
|
||||
'username', 'type', 'type_display', 'ip', 'city', 'user_agent',
|
||||
'mfa', 'mfa_display', 'reason', 'backend',
|
||||
'mfa', 'mfa_display', 'reason', 'reason_display', 'backend',
|
||||
'status', 'status_display',
|
||||
'datetime',
|
||||
]
|
||||
fields = fields_small
|
||||
extra_kwargs = {
|
||||
"user_agent": {'label': _('User agent')}
|
||||
"user_agent": {'label': _('User agent')},
|
||||
"reason_display": {'label': _('Reason')}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.db.models.signals import (
|
||||
post_save, post_delete, m2m_changed, pre_delete
|
||||
post_save, m2m_changed, pre_delete
|
||||
)
|
||||
from django.dispatch import receiver
|
||||
from django.conf import settings
|
||||
|
@ -14,25 +14,25 @@ from rest_framework.renderers import JSONRenderer
|
|||
from rest_framework.request import Request
|
||||
|
||||
from assets.models import Asset, SystemUser
|
||||
from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR
|
||||
from authentication.signals import post_auth_failed, post_auth_success
|
||||
from authentication.utils import check_different_city_login
|
||||
from jumpserver.utils import current_request
|
||||
from common.utils import get_request_ip, get_logger, get_syslogger
|
||||
from users.models import User
|
||||
from users.signals import post_user_change_password
|
||||
from authentication.signals import post_auth_failed, post_auth_success
|
||||
from terminal.models import Session, Command
|
||||
from common.utils.encode import model_to_json
|
||||
from .utils import write_login_log
|
||||
from . import models
|
||||
from .models import OperateLog
|
||||
from orgs.utils import current_org
|
||||
from perms.models import AssetPermission, ApplicationPermission
|
||||
from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR
|
||||
from common.utils import get_request_ip, get_logger, get_syslogger
|
||||
from common.utils.encode import model_to_json
|
||||
|
||||
logger = get_logger(__name__)
|
||||
sys_logger = get_syslogger(__name__)
|
||||
json_render = JSONRenderer()
|
||||
|
||||
|
||||
MODELS_NEED_RECORD = (
|
||||
# users
|
||||
'User', 'UserGroup',
|
||||
|
@ -165,7 +165,6 @@ M2M_NEED_RECORD = {
|
|||
),
|
||||
}
|
||||
|
||||
|
||||
M2M_ACTION = {
|
||||
POST_ADD: 'add',
|
||||
POST_REMOVE: 'remove',
|
||||
|
@ -305,6 +304,7 @@ def generate_data(username, request, login_type=None):
|
|||
@receiver(post_auth_success)
|
||||
def on_user_auth_success(sender, user, request, login_type=None, **kwargs):
|
||||
logger.debug('User login success: {}'.format(user.username))
|
||||
check_different_city_login(user, request)
|
||||
data = generate_data(user.username, request, login_type=login_type)
|
||||
data.update({'mfa': int(user.mfa_enabled), 'status': True})
|
||||
write_login_log(**data)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import csv
|
||||
import codecs
|
||||
from django.http import HttpResponse
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from .const import DEFAULT_CITY
|
||||
from common.utils import validate_ip, get_ip_city
|
||||
|
||||
|
||||
|
@ -27,12 +27,12 @@ def write_content_to_excel(response, header=None, login_logs=None, fields=None):
|
|||
|
||||
def write_login_log(*args, **kwargs):
|
||||
from audits.models import UserLoginLog
|
||||
default_city = _("Unknown")
|
||||
|
||||
ip = kwargs.get('ip') or ''
|
||||
if not (ip and validate_ip(ip)):
|
||||
ip = ip[:15]
|
||||
city = default_city
|
||||
city = DEFAULT_CITY
|
||||
else:
|
||||
city = get_ip_city(ip) or default_city
|
||||
city = get_ip_city(ip) or DEFAULT_CITY
|
||||
kwargs.update({'ip': ip, 'city': city})
|
||||
UserLoginLog.objects.create(**kwargs)
|
||||
|
|
|
@ -23,7 +23,9 @@ from common.drf.api import SerializerMixin
|
|||
from common.permissions import IsSuperUserOrAppUser, IsValidUser, IsSuperUser
|
||||
from orgs.mixins.api import RootOrgViewMixin
|
||||
from common.http import is_true
|
||||
from assets.models import SystemUser
|
||||
from perms.utils.asset.permission import get_asset_system_user_ids_with_actions_by_user
|
||||
from perms.models.asset_permission import Action
|
||||
from authentication.errors import NotHaveUpDownLoadPerm
|
||||
|
||||
from ..serializers import (
|
||||
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
|
||||
|
@ -89,8 +91,14 @@ class ClientProtocolMixin:
|
|||
drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
|
||||
token = self.create_token(user, asset, application, system_user)
|
||||
|
||||
if drives_redirect:
|
||||
options['drivestoredirect:s'] = '*'
|
||||
if drives_redirect and asset:
|
||||
systemuser_actions_mapper = get_asset_system_user_ids_with_actions_by_user(user, asset)
|
||||
actions = systemuser_actions_mapper.get(system_user.id, [])
|
||||
if actions & Action.UPDOWNLOAD:
|
||||
options['drivestoredirect:s'] = '*'
|
||||
else:
|
||||
raise NotHaveUpDownLoadPerm
|
||||
|
||||
options['screen mode id:i'] = '2' if full_screen else '1'
|
||||
address = settings.TERMINAL_RDP_ADDR
|
||||
if not address or address == 'localhost:3389':
|
||||
|
|
|
@ -8,29 +8,12 @@ from django.shortcuts import get_object_or_404
|
|||
|
||||
from common.utils import get_logger
|
||||
from common.permissions import IsOrgAdmin
|
||||
from ..models import LoginConfirmSetting
|
||||
from ..serializers import LoginConfirmSettingSerializer
|
||||
from .. import errors, mixins
|
||||
|
||||
__all__ = ['LoginConfirmSettingUpdateApi', 'TicketStatusApi']
|
||||
__all__ = ['TicketStatusApi']
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class LoginConfirmSettingUpdateApi(UpdateAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = LoginConfirmSettingSerializer
|
||||
|
||||
def get_object(self):
|
||||
from users.models import User
|
||||
user_id = self.kwargs.get('user_id')
|
||||
user = get_object_or_404(User, pk=user_id)
|
||||
defaults = {'user': user}
|
||||
s, created = LoginConfirmSetting.objects.get_or_create(
|
||||
defaults, user=user,
|
||||
)
|
||||
return s
|
||||
|
||||
|
||||
class TicketStatusApi(mixins.AuthMixin, APIView):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
|
|
|
@ -2,17 +2,17 @@
|
|||
#
|
||||
import builtins
|
||||
import time
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.conf import settings
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.generics import CreateAPIView
|
||||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.response import Response
|
||||
|
||||
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 common.permissions import IsValidUser, NeedMFAVerify
|
||||
from users.models.user import MFAType, User
|
||||
from ..serializers import OtpVerifySerializer
|
||||
from .. import serializers
|
||||
from .. import errors
|
||||
|
@ -90,6 +90,13 @@ class SendSMSVerifyCodeApi(AuthMixin, CreateAPIView):
|
|||
permission_classes = (AllowAny,)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
user = self.get_user_from_session()
|
||||
username = request.data.get('username', '')
|
||||
username = username.strip()
|
||||
if username:
|
||||
user = get_object_or_404(User, username=username)
|
||||
else:
|
||||
user = self.get_user_from_session()
|
||||
if not user.mfa_enabled:
|
||||
raise errors.NotEnableMFAError
|
||||
timeout = user.send_sms_code()
|
||||
return Response({'code': 'ok','timeout': timeout})
|
||||
return Response({'code': 'ok', 'timeout': timeout})
|
||||
|
|
|
@ -9,7 +9,7 @@ from rest_framework.response import Response
|
|||
from rest_framework.request import Request
|
||||
from rest_framework.permissions import AllowAny
|
||||
|
||||
from common.utils.timezone import utcnow
|
||||
from common.utils.timezone import utc_now
|
||||
from common.const.http import POST, GET
|
||||
from common.drf.api import JMSGenericViewSet
|
||||
from common.drf.serializers import EmptySerializer
|
||||
|
@ -79,7 +79,7 @@ class SSOViewSet(AuthMixin, JMSGenericViewSet):
|
|||
return HttpResponseRedirect(next_url)
|
||||
|
||||
# 判断是否过期
|
||||
if (utcnow().timestamp() - token.date_created.timestamp()) > settings.AUTH_SSO_AUTHKEY_TTL:
|
||||
if (utc_now().timestamp() - token.date_created.timestamp()) > settings.AUTH_SSO_AUTHKEY_TTL:
|
||||
self.send_auth_signal(success=False, reason='authkey_timeout')
|
||||
return HttpResponseRedirect(next_url)
|
||||
|
||||
|
|
|
@ -6,5 +6,6 @@ class AuthenticationConfig(AppConfig):
|
|||
|
||||
def ready(self):
|
||||
from . import signals_handlers
|
||||
from . import notifications
|
||||
super().ready()
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
from rest_framework import status
|
||||
|
||||
from authentication import sms_verify_code
|
||||
from common.exceptions import JMSException
|
||||
from .signals import post_auth_failed
|
||||
from users.utils import LoginBlockUtil, MFABlockUtils
|
||||
|
@ -78,6 +78,7 @@ mfa_type_failed_msg = _(
|
|||
|
||||
mfa_required_msg = _("MFA required")
|
||||
mfa_unset_msg = _("MFA not set, please set it first")
|
||||
otp_unset_msg = _("OTP not set, please set it first")
|
||||
login_confirm_required_msg = _("Login confirm required")
|
||||
login_confirm_wait_msg = _("Wait login confirm ticket for accept")
|
||||
login_confirm_error_msg = _("Login confirm ticket was {}")
|
||||
|
@ -260,6 +261,13 @@ class LoginIPNotAllowed(ACLError):
|
|||
super().__init__(_("IP is not allowed"), **kwargs)
|
||||
|
||||
|
||||
class TimePeriodNotAllowed(ACLError):
|
||||
def __init__(self, username, request, **kwargs):
|
||||
self.username = username
|
||||
self.request = request
|
||||
super().__init__(_("Time Period is not allowed"), **kwargs)
|
||||
|
||||
|
||||
class LoginConfirmBaseError(NeedMoreInfoError):
|
||||
def __init__(self, ticket_id, **kwargs):
|
||||
self.ticket_id = ticket_id
|
||||
|
@ -348,3 +356,21 @@ class FeiShuNotBound(JMSException):
|
|||
class PasswdInvalid(JMSException):
|
||||
default_code = 'passwd_invalid'
|
||||
default_detail = _('Your password is invalid')
|
||||
|
||||
|
||||
class NotHaveUpDownLoadPerm(JMSException):
|
||||
status_code = status.HTTP_403_FORBIDDEN
|
||||
code = 'not_have_up_down_load_perm'
|
||||
default_detail = _('No upload or download permission')
|
||||
|
||||
|
||||
class NotEnableMFAError(JMSException):
|
||||
default_detail = mfa_unset_msg
|
||||
|
||||
|
||||
class OTPRequiredError(JMSException):
|
||||
default_detail = otp_unset_msg
|
||||
|
||||
def __init__(self, url, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.url = url
|
||||
|
|
|
@ -43,7 +43,7 @@ class UserLoginForm(forms.Form):
|
|||
|
||||
|
||||
class UserCheckOtpCodeForm(forms.Form):
|
||||
code = forms.CharField(label=_('MFA Code'), max_length=6)
|
||||
code = forms.CharField(label=_('MFA Code'), max_length=6, required=False)
|
||||
mfa_type = forms.CharField(label=_('MFA type'), max_length=6)
|
||||
|
||||
|
||||
|
@ -59,7 +59,7 @@ class ChallengeMixin(forms.Form):
|
|||
challenge = forms.CharField(
|
||||
label=_('MFA code'), max_length=6, required=False,
|
||||
widget=forms.TextInput(attrs={
|
||||
'placeholder': _("MFA code"),
|
||||
'placeholder': _("Dynamic code"),
|
||||
'style': 'width: 50%'
|
||||
})
|
||||
)
|
||||
|
@ -69,6 +69,8 @@ def get_user_login_form_cls(*, captcha=False):
|
|||
bases = []
|
||||
if settings.SECURITY_LOGIN_CHALLENGE_ENABLED:
|
||||
bases.append(ChallengeMixin)
|
||||
elif settings.SECURITY_MFA_IN_LOGIN_PAGE:
|
||||
bases.append(UserCheckOtpCodeForm)
|
||||
elif settings.SECURITY_LOGIN_CAPTCHA_ENABLED and captcha:
|
||||
bases.append(CaptchaMixin)
|
||||
bases.append(UserLoginForm)
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
# Generated by Django 3.1.12 on 2021-09-26 11:13
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('authentication', '0004_ssotoken'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='LoginConfirmSetting',
|
||||
),
|
||||
]
|
|
@ -1,12 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import inspect
|
||||
from urllib.parse import urlencode
|
||||
from django.utils.http import urlencode
|
||||
from functools import partial
|
||||
import time
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.conf import settings
|
||||
from django.urls import reverse_lazy
|
||||
from django.contrib import auth
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.contrib.auth import (
|
||||
|
@ -14,9 +15,9 @@ from django.contrib.auth import (
|
|||
PermissionDenied, user_login_failed, _clean_credentials
|
||||
)
|
||||
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 acls.models import LoginACL
|
||||
from users.models import User, MFAType
|
||||
from users.utils import LoginBlockUtil, MFABlockUtils
|
||||
from . import errors
|
||||
|
@ -204,22 +205,24 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
|||
data = request.POST
|
||||
|
||||
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='')
|
||||
ip = self.get_request_ip()
|
||||
self._set_partial_credential_error(username=username, ip=ip, request=request)
|
||||
|
||||
password = password + challenge.strip()
|
||||
if decrypt_passwd:
|
||||
password = self.get_decrypted_password()
|
||||
password = password + challenge.strip()
|
||||
return username, password, public_key, ip, auto_login
|
||||
|
||||
def _check_only_allow_exists_user_auth(self, username):
|
||||
# 仅允许预先存在的用户认证
|
||||
if settings.ONLY_ALLOW_EXIST_USER_AUTH:
|
||||
exist = User.objects.filter(username=username).exists()
|
||||
if not exist:
|
||||
logger.error(f"Only allow exist user auth, login failed: {username}")
|
||||
self.raise_credential_error(errors.reason_user_not_exist)
|
||||
if not settings.ONLY_ALLOW_EXIST_USER_AUTH:
|
||||
return
|
||||
|
||||
exist = User.objects.filter(username=username).exists()
|
||||
if not exist:
|
||||
logger.error(f"Only allow exist user auth, login failed: {username}")
|
||||
self.raise_credential_error(errors.reason_user_not_exist)
|
||||
|
||||
def _check_auth_user_is_valid(self, username, password, public_key):
|
||||
user = authenticate(self.request, username=username, password=password, public_key=public_key)
|
||||
|
@ -231,12 +234,25 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
|||
self.raise_credential_error(errors.reason_user_inactive)
|
||||
return user
|
||||
|
||||
def _check_login_mfa_login_if_need(self, user):
|
||||
request = self.request
|
||||
if hasattr(request, 'data'):
|
||||
data = request.data
|
||||
else:
|
||||
data = request.POST
|
||||
code = data.get('code')
|
||||
mfa_type = data.get('mfa_type')
|
||||
if settings.SECURITY_MFA_IN_LOGIN_PAGE and code and mfa_type:
|
||||
self.check_user_mfa(code, mfa_type, user=user)
|
||||
|
||||
def _check_login_acl(self, user, ip):
|
||||
# ACL 限制用户登录
|
||||
from acls.models import LoginACL
|
||||
is_allowed = LoginACL.allow_user_to_login(user, ip)
|
||||
is_allowed, limit_type = LoginACL.allow_user_to_login(user, ip)
|
||||
if not is_allowed:
|
||||
raise errors.LoginIPNotAllowed(username=user.username, request=self.request)
|
||||
if limit_type == 'ip':
|
||||
raise errors.LoginIPNotAllowed(username=user.username, request=self.request)
|
||||
elif limit_type == 'time':
|
||||
raise errors.TimePeriodNotAllowed(username=user.username, request=self.request)
|
||||
|
||||
def set_login_failed_mark(self):
|
||||
ip = self.get_request_ip()
|
||||
|
@ -255,8 +271,7 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
|||
|
||||
def check_user_auth(self, decrypt_passwd=False):
|
||||
self.check_is_block()
|
||||
request = self.request
|
||||
username, password, public_key, ip, auto_login = self.get_auth_data(decrypt_passwd=decrypt_passwd)
|
||||
username, password, public_key, ip, auto_login = self.get_auth_data(decrypt_passwd)
|
||||
|
||||
self._check_only_allow_exists_user_auth(username)
|
||||
user = self._check_auth_user_is_valid(username, password, public_key)
|
||||
|
@ -266,7 +281,11 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
|||
self._check_passwd_is_too_simple(user, password)
|
||||
self._check_passwd_need_update(user)
|
||||
|
||||
# 校验login-mfa, 如果登录页面上显示 mfa 的话
|
||||
self._check_login_mfa_login_if_need(user)
|
||||
|
||||
LoginBlockUtil(username, ip).clean_failed_count()
|
||||
request = self.request
|
||||
request.session['auth_password'] = 1
|
||||
request.session['user_id'] = str(user.id)
|
||||
request.session['auto_login'] = auto_login
|
||||
|
@ -348,12 +367,11 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
|||
def check_user_mfa_if_need(self, user):
|
||||
if self.request.session.get('auth_mfa'):
|
||||
return
|
||||
|
||||
if settings.OTP_IN_RADIUS:
|
||||
return
|
||||
|
||||
if not user.mfa_enabled:
|
||||
return
|
||||
|
||||
unset, url = user.mfa_enabled_but_not_set()
|
||||
if unset:
|
||||
raise errors.MFAUnsetError(user, self.request, url)
|
||||
|
@ -372,19 +390,29 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
|||
self.request.session['auth_mfa_type'] = ''
|
||||
|
||||
def check_mfa_is_block(self, username, ip, raise_exception=True):
|
||||
if MFABlockUtils(username, ip).is_block():
|
||||
logger.warn('Ip was blocked' + ': ' + username + ':' + ip)
|
||||
exception = errors.BlockMFAError(username=username, request=self.request, ip=ip)
|
||||
if raise_exception:
|
||||
raise exception
|
||||
else:
|
||||
return exception
|
||||
blocked = MFABlockUtils(username, ip).is_block()
|
||||
if not blocked:
|
||||
return
|
||||
logger.warn('Ip was blocked' + ': ' + username + ':' + ip)
|
||||
exception = errors.BlockMFAError(username=username, request=self.request, ip=ip)
|
||||
if raise_exception:
|
||||
raise exception
|
||||
else:
|
||||
return exception
|
||||
|
||||
def check_user_mfa(self, code, mfa_type=MFAType.OTP, user=None):
|
||||
user = user if user else self.get_user_from_session()
|
||||
if not user.mfa_enabled:
|
||||
return
|
||||
|
||||
if not bool(user.otp_secret_key) and mfa_type == MFAType.OTP:
|
||||
self.set_passwd_verify_on_session(user)
|
||||
raise errors.OTPRequiredError(reverse_lazy('authentication:user-otp-enable-bind'))
|
||||
|
||||
def check_user_mfa(self, code, mfa_type=MFAType.OTP):
|
||||
user = self.get_user_from_session()
|
||||
ip = self.get_request_ip()
|
||||
self.check_mfa_is_block(user.username, ip)
|
||||
ok = user.check_mfa(code, mfa_type=mfa_type)
|
||||
|
||||
if ok:
|
||||
self.mark_mfa_ok()
|
||||
return
|
||||
|
@ -437,10 +465,9 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
|||
)
|
||||
|
||||
def check_user_login_confirm_if_need(self, user):
|
||||
if not settings.LOGIN_CONFIRM_ENABLE:
|
||||
return
|
||||
confirm_setting = user.get_login_confirm_setting()
|
||||
if self.request.session.get('auth_confirm') or not confirm_setting:
|
||||
ip = self.get_request_ip()
|
||||
is_allowed, confirm_setting = LoginACL.allow_user_confirm_if_need(user, ip)
|
||||
if self.request.session.get('auth_confirm') or not is_allowed:
|
||||
return
|
||||
self.get_ticket_or_create(confirm_setting)
|
||||
self.check_user_login_confirm()
|
||||
|
@ -468,3 +495,31 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
|||
if args:
|
||||
guard_url = "%s?%s" % (guard_url, args)
|
||||
return redirect(guard_url)
|
||||
|
||||
@staticmethod
|
||||
def get_user_mfa_methods(user=None):
|
||||
otp_enabled = user.otp_secret_key if user else True
|
||||
# 没有用户时,或者有用户并且有电话配置
|
||||
sms_enabled = any([user and user.phone, not user]) \
|
||||
and settings.SMS_ENABLED and settings.XPACK_ENABLED
|
||||
|
||||
methods = [
|
||||
{
|
||||
'name': 'otp',
|
||||
'label': 'MFA',
|
||||
'enable': otp_enabled,
|
||||
'selected': False,
|
||||
},
|
||||
{
|
||||
'name': 'sms',
|
||||
'label': _('SMS'),
|
||||
'enable': sms_enabled,
|
||||
'selected': False,
|
||||
},
|
||||
]
|
||||
|
||||
for item in methods:
|
||||
if item['enable']:
|
||||
item['selected'] = True
|
||||
break
|
||||
return methods
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import uuid
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _, ugettext as __
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.authtoken.models import Token
|
||||
from django.conf import settings
|
||||
|
||||
from common.db import models
|
||||
from common.mixins.models import CommonModelMixin
|
||||
from common.utils import get_object_or_none, get_request_ip, get_ip_city
|
||||
|
||||
|
||||
class AccessKey(models.Model):
|
||||
|
@ -40,56 +37,6 @@ class PrivateToken(Token):
|
|||
verbose_name = _('Private Token')
|
||||
|
||||
|
||||
class LoginConfirmSetting(CommonModelMixin):
|
||||
user = models.OneToOneField('users.User', on_delete=models.CASCADE, verbose_name=_("User"), related_name="login_confirm_setting")
|
||||
reviewers = models.ManyToManyField('users.User', verbose_name=_("Reviewers"), related_name="review_login_confirm_settings", blank=True)
|
||||
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Login Confirm')
|
||||
|
||||
@classmethod
|
||||
def get_user_confirm_setting(cls, user):
|
||||
return get_object_or_none(cls, user=user)
|
||||
|
||||
@staticmethod
|
||||
def construct_confirm_ticket_meta(request=None):
|
||||
if request:
|
||||
login_ip = get_request_ip(request)
|
||||
else:
|
||||
login_ip = ''
|
||||
login_ip = login_ip or '0.0.0.0'
|
||||
login_city = get_ip_city(login_ip)
|
||||
login_datetime = timezone.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
ticket_meta = {
|
||||
'apply_login_ip': login_ip,
|
||||
'apply_login_city': login_city,
|
||||
'apply_login_datetime': login_datetime,
|
||||
}
|
||||
return ticket_meta
|
||||
|
||||
def create_confirm_ticket(self, request=None):
|
||||
from tickets import const
|
||||
from tickets.models import Ticket
|
||||
from orgs.models import Organization
|
||||
ticket_title = _('Login confirm') + ' {}'.format(self.user)
|
||||
ticket_meta = self.construct_confirm_ticket_meta(request)
|
||||
data = {
|
||||
'title': ticket_title,
|
||||
'type': const.TicketType.login_confirm.value,
|
||||
'meta': ticket_meta,
|
||||
'org_id': Organization.ROOT_ID,
|
||||
}
|
||||
ticket = Ticket.objects.create(**data)
|
||||
ticket.create_process_map_and_node(self.reviewers.all())
|
||||
ticket.open(self.user)
|
||||
return ticket
|
||||
|
||||
def __str__(self):
|
||||
reviewers = [u.username for u in self.reviewers.all()]
|
||||
return _('{} need confirm by {}').format(self.user.username, reviewers)
|
||||
|
||||
|
||||
class SSOToken(models.JMSBaseModel):
|
||||
"""
|
||||
类似腾讯企业邮的 [单点登录](https://exmail.qq.com/qy_mng_logic/doc#10036)
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
from notifications.notifications import UserMessage
|
||||
from common.utils import get_logger
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class DifferentCityLoginMessage(UserMessage):
|
||||
def __init__(self, user, ip, city):
|
||||
self.ip = ip
|
||||
self.city = city
|
||||
super().__init__(user)
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
now_local = timezone.localtime(timezone.now())
|
||||
now = now_local.strftime("%Y-%m-%d %H:%M:%S")
|
||||
subject = _('Different city login reminder')
|
||||
context = dict(
|
||||
subject=subject,
|
||||
name=self.user.name,
|
||||
username=self.user.username,
|
||||
ip=self.ip,
|
||||
time=now,
|
||||
city=self.city,
|
||||
)
|
||||
message = render_to_string('authentication/_msg_different_city.html', context)
|
||||
return {
|
||||
'subject': subject,
|
||||
'message': message
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
from users.models import User
|
||||
user = User.objects.first()
|
||||
ip = '8.8.8.8'
|
||||
city = '洛杉矶'
|
||||
return cls(user, ip, city)
|
|
@ -10,12 +10,11 @@ from applications.models import Application
|
|||
from users.serializers import UserProfileSerializer
|
||||
from assets.serializers import ProtocolsField
|
||||
from perms.serializers.asset.permission import ActionsField
|
||||
from .models import AccessKey, LoginConfirmSetting
|
||||
|
||||
from .models import AccessKey
|
||||
|
||||
__all__ = [
|
||||
'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer',
|
||||
'MFAChallengeSerializer', 'LoginConfirmSettingSerializer', 'SSOTokenSerializer',
|
||||
'MFAChallengeSerializer', 'SSOTokenSerializer',
|
||||
'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer',
|
||||
'PasswordVerifySerializer', 'MFASelectTypeSerializer',
|
||||
]
|
||||
|
@ -92,13 +91,6 @@ class MFAChallengeSerializer(serializers.Serializer):
|
|||
pass
|
||||
|
||||
|
||||
class LoginConfirmSettingSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = LoginConfirmSetting
|
||||
fields = ['id', 'user', 'reviewers', 'date_created', 'date_updated']
|
||||
read_only_fields = ['date_created', 'date_updated']
|
||||
|
||||
|
||||
class SSOTokenSerializer(serializers.Serializer):
|
||||
username = serializers.CharField(write_only=True)
|
||||
login_url = serializers.CharField(read_only=True)
|
||||
|
@ -201,4 +193,3 @@ class ConnectionTokenSecretSerializer(serializers.Serializer):
|
|||
gateway = ConnectionTokenGatewaySerializer(read_only=True)
|
||||
actions = ActionsField()
|
||||
expired_at = serializers.IntegerField()
|
||||
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
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
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
<div class="col-sm-6">
|
||||
<div class="input-group-prepend">
|
||||
{% if audio %}
|
||||
<a title="{% trans "Play CAPTCHA as audio file" %}" href="{{ audio }}">
|
||||
<a title="{% trans "Play CAPTCHA as audio file" %}" href="{{ audio }}"></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "django/forms/widgets/multiwidget.html" %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
{% load i18n %}
|
||||
<p>
|
||||
{% trans 'Hello' %} {{ name }},
|
||||
</p>
|
||||
<p>
|
||||
{% trans 'Your account has remote login behavior, please pay attention' %}
|
||||
</p>
|
||||
<p>
|
||||
<b>{% trans 'Username' %}:</b> {{ username }}<br>
|
||||
<b>{% trans 'Login time' %}:</b> {{ time }}<br>
|
||||
<b>{% trans 'Login city' %}:</b> {{ city }}({{ ip }})
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<small>
|
||||
{% trans 'If you suspect that the login behavior is abnormal, please modify the account password in time.' %}
|
||||
</small>
|
||||
</p>
|
|
@ -0,0 +1,15 @@
|
|||
{% load i18n %}
|
||||
{% trans 'Hello' %} {{ user.name }},
|
||||
<br>
|
||||
{% trans 'Please click the link below to reset your password, if not your request, concern your account security' %}
|
||||
<br>
|
||||
<br>
|
||||
<a href="{{ rest_password_url }}?token={{ rest_password_token}}" class='showLink'>{% trans 'Click here reset password' %}</a>
|
||||
<br>
|
||||
<br>
|
||||
{% trans 'This link is valid for 1 hour. After it expires,' %} <a href="{{ forget_password_url }}?email={{ user.email }}">{% trans 'request new one' %}</a>
|
||||
<br>
|
||||
---
|
||||
<br>
|
||||
<a href="{{ login_url }}">{% trans 'Login direct' %}</a>
|
||||
<br>
|
|
@ -0,0 +1,18 @@
|
|||
{% load i18n %}
|
||||
|
||||
<p>{% trans 'Hello' %}: {{ name }},</p>
|
||||
|
||||
<p>
|
||||
{% trans 'Your password has just been successfully updated.' %}
|
||||
</p>
|
||||
<p>
|
||||
{% trans 'IP' %}: {{ ip_address }} <br />
|
||||
{% trans 'Browser' %}: {{ browser }}
|
||||
</p>
|
||||
<p>---</p>
|
||||
<p>
|
||||
<small>
|
||||
{% trans 'If the password update was not initiated by you, your account may have security issues.' %} <br />
|
||||
{% trans 'If you have any questions, you can contact the administrator.' %}
|
||||
</small>
|
||||
</p>
|
|
@ -10,23 +10,15 @@
|
|||
{{ JMS_TITLE }}
|
||||
</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{% include '_head_css_js.html' %}
|
||||
<!-- Stylesheets -->
|
||||
<link href="{% static 'css/bootstrap.min.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'css/font-awesome.min.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'css/bootstrap-style.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'css/login-style.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'css/style.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'css/jumpserver.css' %}" rel="stylesheet">
|
||||
|
||||
<!-- scripts -->
|
||||
<script src="{% static 'js/jquery-3.1.1.min.js' %}"></script>
|
||||
<script src="{% static 'js/plugins/sweetalert/sweetalert.min.js' %}"></script>
|
||||
<script src="{% static 'js/bootstrap.min.js' %}"></script>
|
||||
<script src="{% static 'js/plugins/datatables/datatables.min.js' %}"></script>
|
||||
<script src="{% static "js/jumpserver.js" %}"></script>
|
||||
|
||||
<style>
|
||||
.login-content {
|
||||
box-shadow: 0 5px 5px -3px rgb(0 0 0 / 20%), 0 8px 10px 1px rgb(0 0 0 / 14%), 0 3px 14px 2px rgb(0 0 0 / 12%);
|
||||
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.15), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.help-block {
|
||||
|
@ -49,17 +41,22 @@
|
|||
}
|
||||
|
||||
.login-content {
|
||||
height: 472px;
|
||||
width: 984px;
|
||||
height: 490px;
|
||||
width: 1066px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
margin-top: calc((100vh - 470px) / 3);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f2f2f2;
|
||||
height: calc(100vh - (100vh - 470px) / 3);
|
||||
}
|
||||
|
||||
.captcha {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.right-image-box {
|
||||
height: 100%;
|
||||
width: 50%;
|
||||
|
@ -73,18 +70,10 @@
|
|||
width: 50%;
|
||||
}
|
||||
|
||||
.captcha {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.red-fonts {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.field-error {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.form-group.has-error {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
@ -109,14 +98,6 @@
|
|||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.radio, .checkbox {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#github_star {
|
||||
float: right;
|
||||
margin: 10px 10px 0 0;
|
||||
}
|
||||
.more-login-item {
|
||||
border-right: 1px dashed #dedede;
|
||||
padding-left: 5px;
|
||||
|
@ -127,11 +108,18 @@
|
|||
border: none;
|
||||
}
|
||||
|
||||
.select-con {
|
||||
width: 22%;
|
||||
}
|
||||
|
||||
.mfa-div {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="login-content ">
|
||||
<div class="login-content">
|
||||
<div class="right-image-box">
|
||||
<a href="{% if not XPACK_ENABLED %}https://github.com/jumpserver/jumpserver{% endif %}">
|
||||
<img src="{{ LOGIN_IMAGE_URL }}" style="height: 100%; width: 100%"/>
|
||||
|
@ -146,11 +134,9 @@
|
|||
<form id="login-form" action="" method="post" role="form" novalidate="novalidate">
|
||||
{% csrf_token %}
|
||||
<div style="line-height: 17px;margin-bottom: 20px;color: #999999;">
|
||||
{% if form.errors %}
|
||||
<p class="help-block">
|
||||
{% if form.non_field_errors %}
|
||||
{{ form.non_field_errors.as_text }}
|
||||
{% endif %}
|
||||
{% if form.non_field_errors %}
|
||||
<p class="help-block red-fonts">
|
||||
{{ form.non_field_errors.as_text }}
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="welcome-message">
|
||||
|
@ -172,6 +158,10 @@
|
|||
</div>
|
||||
{% if form.challenge %}
|
||||
{% bootstrap_field form.challenge show_label=False %}
|
||||
{% elif form.mfa_type %}
|
||||
<div class="form-group" style="display: flex">
|
||||
{% include '_mfa_otp_login.html' %}
|
||||
</div>
|
||||
{% elif form.captcha %}
|
||||
<div class="captch-field">
|
||||
{% bootstrap_field form.captcha show_label=False %}
|
||||
|
@ -197,35 +187,15 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
{% if AUTH_OPENID or AUTH_CAS or AUTH_WECOM or AUTH_DINGTALK or AUTH_FEISHU %}
|
||||
{% if auth_methods %}
|
||||
<div class="hr-line-dashed"></div>
|
||||
<div style="display: inline-block; float: left">
|
||||
<b class="text-muted text-left" >{% trans "More login options" %}</b>
|
||||
{% if AUTH_OPENID %}
|
||||
<a href="{% url 'authentication:openid:login' %}" class="more-login-item">
|
||||
<i class="fa fa-openid"></i> {% trans 'OpenID' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if AUTH_CAS %}
|
||||
<a href="{% url 'authentication:cas:cas-login' %}" class="more-login-item">
|
||||
<i class="fa"><img src="{{ LOGIN_CAS_LOGO_URL }}" height="13" width="13"></i> {% trans 'CAS' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if AUTH_WECOM %}
|
||||
<a href="{% url 'authentication:wecom-qr-login' %}" class="more-login-item">
|
||||
<i class="fa"><img src="{{ LOGIN_WECOM_LOGO_URL }}" height="13" width="13"></i> {% trans 'WeCom' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if AUTH_DINGTALK %}
|
||||
<a href="{% url 'authentication:dingtalk-qr-login' %}" class="more-login-item">
|
||||
<i class="fa"><img src="{{ LOGIN_DINGTALK_LOGO_URL }}" height="13" width="13"></i> {% trans 'DingTalk' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if AUTH_FEISHU %}
|
||||
<a href="{% url 'authentication:feishu-qr-login' %}" class="more-login-item">
|
||||
<i class="fa"><img src="{{ LOGIN_FEISHU_LOGO_URL }}" height="13" width="13"></i> {% trans 'FeiShu' %}
|
||||
<b class="text-muted text-left" >{% trans "More login options" %}</b>
|
||||
{% for method in auth_methods %}
|
||||
<a href="{{ method.url }}" class="more-login-item">
|
||||
<i class="fa"><img src="{{ method.logo }}" height="13" width="13"></i> {{ method.name }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center" style="display: inline-block;">
|
||||
|
@ -255,7 +225,7 @@
|
|||
var password = $('#password').val(); //明文密码
|
||||
var passwordEncrypted = encryptLoginPassword(password, rsaPublicKey)
|
||||
$('#password-hidden').val(passwordEncrypted); //返回给密码输入input
|
||||
$('#login-form').submit();//post提交
|
||||
$('#login-form').submit(); //post提交
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
|
|
|
@ -13,78 +13,19 @@
|
|||
<p class="red-fonts">{{ form.code.errors.as_text }}</p>
|
||||
{% endif %}
|
||||
<div class="form-group">
|
||||
<select id="verify-method-select" name="mfa_type" class="form-control" onchange="select_change(this.value)">
|
||||
{% for method in methods %}
|
||||
<option value="{{ method.name }}" {% if method.selected %} selected {% endif %} {% if not method.enable %} disabled {% endif %}>{{ method.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% include '_mfa_otp_login.html' %}
|
||||
</div>
|
||||
<div class="form-group" style="display: flex">
|
||||
|
||||
<input id="mfa-code" required type="text" class="form-control" name="code" placeholder="{% trans 'Please enter the verification code' %}" autofocus="autofocus">
|
||||
<button id='send-sms-verify-code' type="button" class="btn btn-info full-width m-b" onclick="sendSMSVerifyCode()" style="width: 150px!important;">{% trans 'Send verification code' %}</button>
|
||||
|
||||
</div>
|
||||
|
||||
<button id='submit_button' type="submit" class="btn btn-primary block full-width m-b">{% trans 'Next' %}</button>
|
||||
<button id='submit_button' type="submit"
|
||||
class="btn btn-primary block full-width m-b">{% trans 'Next' %}</button>
|
||||
<div>
|
||||
<small>{% trans "Can't provide security? Please contact the administrator!" %}</small>
|
||||
</div>
|
||||
</form>
|
||||
<style type="text/css">
|
||||
.disabledBtn {
|
||||
background: #e6e4e4!important;
|
||||
border-color: #d8d5d5!important;
|
||||
color: #949191!important;
|
||||
}
|
||||
.mfa-div {
|
||||
margin-top: 15px;
|
||||
}
|
||||
</style>
|
||||
<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";
|
||||
currentBtn.disabled = false;
|
||||
}
|
||||
else {
|
||||
currentBtn.style.display = "none";
|
||||
currentBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
function sendSMSVerifyCode(){
|
||||
var currentBtn = document.getElementById('send-sms-verify-code');
|
||||
var time = 60
|
||||
var url = "{% url 'api-auth:sms-verify-code-send' %}";
|
||||
requestApi({
|
||||
url: url,
|
||||
method: "POST",
|
||||
success: function (data) {
|
||||
currentBtn.innerHTML = `{% trans 'Wait: ' %} ${time}`;
|
||||
currentBtn.disabled = true
|
||||
currentBtn.classList.add("disabledBtn" )
|
||||
var TimeInterval = setInterval(()=>{
|
||||
--time
|
||||
currentBtn.innerHTML = `{% trans 'Wait: ' %} ${time}`;
|
||||
if(time === 0) {
|
||||
currentBtn.innerHTML = "{% trans 'Send verification code' %}"
|
||||
currentBtn.disabled = false
|
||||
currentBtn.classList.remove("disabledBtn")
|
||||
clearInterval(TimeInterval)
|
||||
}
|
||||
},1000)
|
||||
alert("{% trans 'The verification code has been sent' %}");
|
||||
},
|
||||
error: function (text, data) {
|
||||
alert(data.detail)
|
||||
},
|
||||
flash_message: false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -13,7 +13,6 @@ router.register('connection-token', api.UserConnectionTokenViewSet, 'connection-
|
|||
|
||||
|
||||
urlpatterns = [
|
||||
# path('token/', api.UserToken.as_view(), name='user-token'),
|
||||
path('wecom/qr/unbind/', api.WeComQRUnBindForUserApi.as_view(), name='wecom-qr-unbind'),
|
||||
path('wecom/qr/unbind/<uuid:user_id>/', api.WeComQRUnBindForAdminApi.as_view(), name='wecom-qr-unbind-for-admin'),
|
||||
|
||||
|
@ -32,7 +31,6 @@ urlpatterns = [
|
|||
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('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')
|
||||
]
|
||||
|
||||
urlpatterns += router.urls
|
||||
|
|
|
@ -22,24 +22,18 @@ urlpatterns = [
|
|||
path('password/reset/', users_view.UserResetPasswordView.as_view(), name='reset-password'),
|
||||
path('password/verify/', users_view.UserVerifyPasswordView.as_view(), name='user-verify-password'),
|
||||
|
||||
path('wecom/bind/success-flash-msg/', views.FlashWeComBindSucceedMsgView.as_view(), name='wecom-bind-success-flash-msg'),
|
||||
path('wecom/bind/failed-flash-msg/', views.FlashWeComBindFailedMsgView.as_view(), name='wecom-bind-failed-flash-msg'),
|
||||
path('wecom/bind/start/', views.WeComEnableStartView.as_view(), name='wecom-bind-start'),
|
||||
path('wecom/qr/bind/', views.WeComQRBindView.as_view(), name='wecom-qr-bind'),
|
||||
path('wecom/qr/login/', views.WeComQRLoginView.as_view(), name='wecom-qr-login'),
|
||||
path('wecom/qr/bind/<uuid:user_id>/callback/', views.WeComQRBindCallbackView.as_view(), name='wecom-qr-bind-callback'),
|
||||
path('wecom/qr/login/callback/', views.WeComQRLoginCallbackView.as_view(), name='wecom-qr-login-callback'),
|
||||
|
||||
path('dingtalk/bind/success-flash-msg/', views.FlashDingTalkBindSucceedMsgView.as_view(), name='dingtalk-bind-success-flash-msg'),
|
||||
path('dingtalk/bind/failed-flash-msg/', views.FlashDingTalkBindFailedMsgView.as_view(), name='dingtalk-bind-failed-flash-msg'),
|
||||
path('dingtalk/bind/start/', views.DingTalkEnableStartView.as_view(), name='dingtalk-bind-start'),
|
||||
path('dingtalk/qr/bind/', views.DingTalkQRBindView.as_view(), name='dingtalk-qr-bind'),
|
||||
path('dingtalk/qr/login/', views.DingTalkQRLoginView.as_view(), name='dingtalk-qr-login'),
|
||||
path('dingtalk/qr/bind/<uuid:user_id>/callback/', views.DingTalkQRBindCallbackView.as_view(), name='dingtalk-qr-bind-callback'),
|
||||
path('dingtalk/qr/login/callback/', views.DingTalkQRLoginCallbackView.as_view(), name='dingtalk-qr-login-callback'),
|
||||
|
||||
path('feishu/bind/success-flash-msg/', views.FlashDingTalkBindSucceedMsgView.as_view(), name='feishu-bind-success-flash-msg'),
|
||||
path('feishu/bind/failed-flash-msg/', views.FlashDingTalkBindFailedMsgView.as_view(), name='feishu-bind-failed-flash-msg'),
|
||||
path('feishu/bind/start/', views.FeiShuEnableStartView.as_view(), name='feishu-bind-start'),
|
||||
path('feishu/qr/bind/', views.FeiShuQRBindView.as_view(), name='feishu-qr-bind'),
|
||||
path('feishu/qr/login/', views.FeiShuQRLoginView.as_view(), name='feishu-qr-login'),
|
||||
|
|
|
@ -4,6 +4,12 @@ import base64
|
|||
from Cryptodome.PublicKey import RSA
|
||||
from Cryptodome.Cipher import PKCS1_v1_5
|
||||
from Cryptodome import Random
|
||||
|
||||
from .notifications import DifferentCityLoginMessage
|
||||
from audits.models import UserLoginLog
|
||||
from audits.const import DEFAULT_CITY
|
||||
from common.utils import get_request_ip
|
||||
from common.utils import validate_ip, get_ip_city
|
||||
from common.utils import get_logger
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
@ -43,3 +49,16 @@ def rsa_decrypt(cipher_text, rsa_private_key=None):
|
|||
cipher_decoded = base64.b16decode(hex_fixed.upper())
|
||||
message = cipher.decrypt(cipher_decoded, b'error').decode()
|
||||
return message
|
||||
|
||||
|
||||
def check_different_city_login(user, request):
|
||||
ip = get_request_ip(request) or '0.0.0.0'
|
||||
|
||||
if not (ip and validate_ip(ip)):
|
||||
city = DEFAULT_CITY
|
||||
else:
|
||||
city = get_ip_city(ip) or DEFAULT_CITY
|
||||
|
||||
last_user_login = UserLoginLog.objects.filter(username=user.username, status=True).first()
|
||||
if last_user_login and last_user_login.city != city:
|
||||
DifferentCityLoginMessage(user, ip, city).publish_async()
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import urllib
|
||||
|
||||
from django.http.response import HttpResponseRedirect
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.generic import TemplateView
|
||||
from urllib.parse import urlencode
|
||||
from django.views import View
|
||||
from django.conf import settings
|
||||
from django.http.request import HttpRequest
|
||||
|
@ -15,7 +11,7 @@ from rest_framework.exceptions import APIException
|
|||
from users.views import UserVerifyPasswordView
|
||||
from users.utils import is_auth_password_time_valid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.utils import get_logger, FlashMessageUtil
|
||||
from common.utils.random import random_string
|
||||
from common.utils.django import reverse, get_object_or_none
|
||||
from common.message.backends.dingtalk import URL
|
||||
|
@ -39,7 +35,7 @@ class DingTalkQRMixin(PermissionsMixin, View):
|
|||
msg = e.detail['errmsg']
|
||||
except Exception:
|
||||
msg = _('DingTalk Error, Please contact your system administrator')
|
||||
return self.get_failed_reponse(
|
||||
return self.get_failed_response(
|
||||
'/',
|
||||
_('DingTalk Error'),
|
||||
msg
|
||||
|
@ -53,8 +49,8 @@ class DingTalkQRMixin(PermissionsMixin, View):
|
|||
return True
|
||||
|
||||
def get_verify_state_failed_response(self, redirect_uri):
|
||||
msg = _("You've been hacked")
|
||||
return self.get_failed_reponse(redirect_uri, msg, msg)
|
||||
msg = _("The system configuration is incorrect. Please contact your administrator")
|
||||
return self.get_failed_response(redirect_uri, msg, msg)
|
||||
|
||||
def get_qr_url(self, redirect_uri):
|
||||
state = random_string(16)
|
||||
|
@ -67,30 +63,32 @@ class DingTalkQRMixin(PermissionsMixin, View):
|
|||
'state': state,
|
||||
'redirect_uri': redirect_uri,
|
||||
}
|
||||
url = URL.QR_CONNECT + '?' + urllib.parse.urlencode(params)
|
||||
url = URL.QR_CONNECT + '?' + urlencode(params)
|
||||
return url
|
||||
|
||||
def get_success_reponse(self, redirect_url, title, msg):
|
||||
ok_flash_msg_url = reverse('authentication:dingtalk-bind-success-flash-msg')
|
||||
ok_flash_msg_url += '?' + urllib.parse.urlencode({
|
||||
'redirect_url': redirect_url,
|
||||
@staticmethod
|
||||
def get_success_response(redirect_url, title, msg):
|
||||
message_data = {
|
||||
'title': title,
|
||||
'msg': msg
|
||||
})
|
||||
return HttpResponseRedirect(ok_flash_msg_url)
|
||||
'message': msg,
|
||||
'interval': 5,
|
||||
'redirect_url': redirect_url,
|
||||
}
|
||||
return FlashMessageUtil.gen_and_redirect_to(message_data)
|
||||
|
||||
def get_failed_reponse(self, redirect_url, title, msg):
|
||||
failed_flash_msg_url = reverse('authentication:dingtalk-bind-failed-flash-msg')
|
||||
failed_flash_msg_url += '?' + urllib.parse.urlencode({
|
||||
'redirect_url': redirect_url,
|
||||
@staticmethod
|
||||
def get_failed_response(redirect_url, title, msg):
|
||||
message_data = {
|
||||
'title': title,
|
||||
'msg': msg
|
||||
})
|
||||
return HttpResponseRedirect(failed_flash_msg_url)
|
||||
'error': msg,
|
||||
'interval': 5,
|
||||
'redirect_url': redirect_url,
|
||||
}
|
||||
return FlashMessageUtil.gen_and_redirect_to(message_data)
|
||||
|
||||
def get_already_bound_response(self, redirect_url):
|
||||
msg = _('DingTalk is already bound')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
response = self.get_failed_response(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
|
||||
|
@ -103,11 +101,11 @@ class DingTalkQRBindView(DingTalkQRMixin, View):
|
|||
|
||||
if not is_auth_password_time_valid(request.session):
|
||||
msg = _('Please verify your password first')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
response = self.get_failed_response(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
redirect_uri = reverse('authentication:dingtalk-qr-bind-callback', kwargs={'user_id': user.id}, external=True)
|
||||
redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url})
|
||||
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
|
||||
|
||||
url = self.get_qr_url(redirect_uri)
|
||||
return HttpResponseRedirect(url)
|
||||
|
@ -127,7 +125,7 @@ class DingTalkQRBindCallbackView(DingTalkQRMixin, View):
|
|||
if user is None:
|
||||
logger.error(f'DingTalkQR bind callback error, user_id invalid: user_id={user_id}')
|
||||
msg = _('Invalid user_id')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
response = self.get_failed_response(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
if user.dingtalk_id:
|
||||
|
@ -143,7 +141,7 @@ class DingTalkQRBindCallbackView(DingTalkQRMixin, View):
|
|||
|
||||
if not userid:
|
||||
msg = _('DingTalk query user failed')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
response = self.get_failed_response(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
try:
|
||||
|
@ -152,12 +150,12 @@ class DingTalkQRBindCallbackView(DingTalkQRMixin, View):
|
|||
except IntegrityError as e:
|
||||
if e.args[0] == 1062:
|
||||
msg = _('The DingTalk is already bound to another user')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
response = self.get_failed_response(redirect_url, msg, msg)
|
||||
return response
|
||||
raise e
|
||||
|
||||
msg = _('Binding DingTalk successfully')
|
||||
response = self.get_success_reponse(redirect_url, msg, msg)
|
||||
response = self.get_success_response(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
|
||||
|
@ -169,7 +167,7 @@ class DingTalkEnableStartView(UserVerifyPasswordView):
|
|||
|
||||
success_url = reverse('authentication:dingtalk-qr-bind')
|
||||
|
||||
success_url += '?' + urllib.parse.urlencode({
|
||||
success_url += '?' + urlencode({
|
||||
'redirect_url': redirect_url or referer
|
||||
})
|
||||
|
||||
|
@ -183,7 +181,7 @@ class DingTalkQRLoginView(DingTalkQRMixin, View):
|
|||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
redirect_uri = reverse('authentication:dingtalk-qr-login-callback', external=True)
|
||||
redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url})
|
||||
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
|
||||
|
||||
url = self.get_qr_url(redirect_uri)
|
||||
return HttpResponseRedirect(url)
|
||||
|
@ -209,14 +207,14 @@ class DingTalkQRLoginCallbackView(AuthMixin, DingTalkQRMixin, View):
|
|||
if not userid:
|
||||
# 正常流程不会出这个错误,hack 行为
|
||||
msg = _('Failed to get user from DingTalk')
|
||||
response = self.get_failed_reponse(login_url, title=msg, msg=msg)
|
||||
response = self.get_failed_response(login_url, title=msg, msg=msg)
|
||||
return response
|
||||
|
||||
user = get_object_or_none(User, dingtalk_id=userid)
|
||||
if user is None:
|
||||
title = _('DingTalk is not bound')
|
||||
msg = _('Please login with a password and then bind the DingTalk')
|
||||
response = self.get_failed_reponse(login_url, title=title, msg=msg)
|
||||
response = self.get_failed_response(login_url, title=title, msg=msg)
|
||||
return response
|
||||
|
||||
try:
|
||||
|
@ -224,43 +222,7 @@ class DingTalkQRLoginCallbackView(AuthMixin, DingTalkQRMixin, View):
|
|||
except errors.AuthFailedError as e:
|
||||
self.set_login_failed_mark()
|
||||
msg = e.msg
|
||||
response = self.get_failed_reponse(login_url, title=msg, msg=msg)
|
||||
response = self.get_failed_response(login_url, title=msg, msg=msg)
|
||||
return response
|
||||
|
||||
return self.redirect_to_guard_view()
|
||||
|
||||
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class FlashDingTalkBindSucceedMsgView(TemplateView):
|
||||
template_name = 'flash_message_standalone.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
title = request.GET.get('title')
|
||||
msg = request.GET.get('msg')
|
||||
|
||||
context = {
|
||||
'title': title or _('Binding DingTalk successfully'),
|
||||
'messages': msg or _('Binding DingTalk successfully'),
|
||||
'interval': 5,
|
||||
'redirect_url': request.GET.get('redirect_url'),
|
||||
'auto_redirect': True,
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
|
||||
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class FlashDingTalkBindFailedMsgView(TemplateView):
|
||||
template_name = 'flash_message_standalone.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
title = request.GET.get('title')
|
||||
msg = request.GET.get('msg')
|
||||
|
||||
context = {
|
||||
'title': title or _('Binding DingTalk failed'),
|
||||
'messages': msg or _('Binding DingTalk failed'),
|
||||
'interval': 5,
|
||||
'redirect_url': request.GET.get('redirect_url'),
|
||||
'auto_redirect': True,
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import urllib
|
||||
|
||||
from django.http.response import HttpResponseRedirect, HttpResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.http.response import HttpResponseRedirect
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.generic import TemplateView
|
||||
from urllib.parse import urlencode
|
||||
from django.views import View
|
||||
from django.conf import settings
|
||||
from django.http.request import HttpRequest
|
||||
|
@ -15,7 +11,7 @@ from rest_framework.exceptions import APIException
|
|||
from users.utils import is_auth_password_time_valid
|
||||
from users.views import UserVerifyPasswordView
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.utils import get_logger, FlashMessageUtil
|
||||
from common.utils.random import random_string
|
||||
from common.utils.django import reverse, get_object_or_none
|
||||
from common.mixins.views import PermissionsMixin
|
||||
|
@ -35,7 +31,7 @@ class FeiShuQRMixin(PermissionsMixin, View):
|
|||
return super().dispatch(request, *args, **kwargs)
|
||||
except APIException as e:
|
||||
msg = str(e.detail)
|
||||
return self.get_failed_reponse(
|
||||
return self.get_failed_response(
|
||||
'/',
|
||||
_('FeiShu Error'),
|
||||
msg
|
||||
|
@ -49,8 +45,8 @@ class FeiShuQRMixin(PermissionsMixin, View):
|
|||
return True
|
||||
|
||||
def get_verify_state_failed_response(self, redirect_uri):
|
||||
msg = _("You've been hacked")
|
||||
return self.get_failed_reponse(redirect_uri, msg, msg)
|
||||
msg = _("The system configuration is incorrect. Please contact your administrator")
|
||||
return self.get_failed_response(redirect_uri, msg, msg)
|
||||
|
||||
def get_qr_url(self, redirect_uri):
|
||||
state = random_string(16)
|
||||
|
@ -61,30 +57,32 @@ class FeiShuQRMixin(PermissionsMixin, View):
|
|||
'state': state,
|
||||
'redirect_uri': redirect_uri,
|
||||
}
|
||||
url = URL.AUTHEN + '?' + urllib.parse.urlencode(params)
|
||||
url = URL.AUTHEN + '?' + urlencode(params)
|
||||
return url
|
||||
|
||||
def get_success_reponse(self, redirect_url, title, msg):
|
||||
ok_flash_msg_url = reverse('authentication:feishu-bind-success-flash-msg')
|
||||
ok_flash_msg_url += '?' + urllib.parse.urlencode({
|
||||
'redirect_url': redirect_url,
|
||||
@staticmethod
|
||||
def get_success_response(redirect_url, title, msg):
|
||||
message_data = {
|
||||
'title': title,
|
||||
'msg': msg
|
||||
})
|
||||
return HttpResponseRedirect(ok_flash_msg_url)
|
||||
'message': msg,
|
||||
'interval': 5,
|
||||
'redirect_url': redirect_url,
|
||||
}
|
||||
return FlashMessageUtil.gen_and_redirect_to(message_data)
|
||||
|
||||
def get_failed_reponse(self, redirect_url, title, msg):
|
||||
failed_flash_msg_url = reverse('authentication:feishu-bind-failed-flash-msg')
|
||||
failed_flash_msg_url += '?' + urllib.parse.urlencode({
|
||||
'redirect_url': redirect_url,
|
||||
@staticmethod
|
||||
def get_failed_response(redirect_url, title, msg):
|
||||
message_data = {
|
||||
'title': title,
|
||||
'msg': msg
|
||||
})
|
||||
return HttpResponseRedirect(failed_flash_msg_url)
|
||||
'error': msg,
|
||||
'interval': 5,
|
||||
'redirect_url': redirect_url,
|
||||
}
|
||||
return FlashMessageUtil.gen_and_redirect_to(message_data)
|
||||
|
||||
def get_already_bound_response(self, redirect_url):
|
||||
msg = _('FeiShu is already bound')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
response = self.get_failed_response(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
|
||||
|
@ -97,11 +95,11 @@ class FeiShuQRBindView(FeiShuQRMixin, View):
|
|||
|
||||
if not is_auth_password_time_valid(request.session):
|
||||
msg = _('Please verify your password first')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
response = self.get_failed_response(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
redirect_uri = reverse('authentication:feishu-qr-bind-callback', external=True)
|
||||
redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url})
|
||||
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
|
||||
|
||||
url = self.get_qr_url(redirect_uri)
|
||||
return HttpResponseRedirect(url)
|
||||
|
@ -131,7 +129,7 @@ class FeiShuQRBindCallbackView(FeiShuQRMixin, View):
|
|||
|
||||
if not user_id:
|
||||
msg = _('FeiShu query user failed')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
response = self.get_failed_response(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
try:
|
||||
|
@ -140,12 +138,12 @@ class FeiShuQRBindCallbackView(FeiShuQRMixin, View):
|
|||
except IntegrityError as e:
|
||||
if e.args[0] == 1062:
|
||||
msg = _('The FeiShu is already bound to another user')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
response = self.get_failed_response(redirect_url, msg, msg)
|
||||
return response
|
||||
raise e
|
||||
|
||||
msg = _('Binding FeiShu successfully')
|
||||
response = self.get_success_reponse(redirect_url, msg, msg)
|
||||
response = self.get_success_response(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
|
||||
|
@ -157,7 +155,7 @@ class FeiShuEnableStartView(UserVerifyPasswordView):
|
|||
|
||||
success_url = reverse('authentication:feishu-qr-bind')
|
||||
|
||||
success_url += '?' + urllib.parse.urlencode({
|
||||
success_url += '?' + urlencode({
|
||||
'redirect_url': redirect_url or referer
|
||||
})
|
||||
|
||||
|
@ -171,7 +169,7 @@ class FeiShuQRLoginView(FeiShuQRMixin, View):
|
|||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
redirect_uri = reverse('authentication:feishu-qr-login-callback', external=True)
|
||||
redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url})
|
||||
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
|
||||
|
||||
url = self.get_qr_url(redirect_uri)
|
||||
return HttpResponseRedirect(url)
|
||||
|
@ -196,14 +194,14 @@ class FeiShuQRLoginCallbackView(AuthMixin, FeiShuQRMixin, View):
|
|||
if not user_id:
|
||||
# 正常流程不会出这个错误,hack 行为
|
||||
msg = _('Failed to get user from FeiShu')
|
||||
response = self.get_failed_reponse(login_url, title=msg, msg=msg)
|
||||
response = self.get_failed_response(login_url, title=msg, msg=msg)
|
||||
return response
|
||||
|
||||
user = get_object_or_none(User, feishu_id=user_id)
|
||||
if user is None:
|
||||
title = _('FeiShu is not bound')
|
||||
msg = _('Please login with a password and then bind the FeiShu')
|
||||
response = self.get_failed_reponse(login_url, title=title, msg=msg)
|
||||
response = self.get_failed_response(login_url, title=title, msg=msg)
|
||||
return response
|
||||
|
||||
try:
|
||||
|
@ -211,43 +209,7 @@ class FeiShuQRLoginCallbackView(AuthMixin, FeiShuQRMixin, View):
|
|||
except errors.AuthFailedError as e:
|
||||
self.set_login_failed_mark()
|
||||
msg = e.msg
|
||||
response = self.get_failed_reponse(login_url, title=msg, msg=msg)
|
||||
response = self.get_failed_response(login_url, title=msg, msg=msg)
|
||||
return response
|
||||
|
||||
return self.redirect_to_guard_view()
|
||||
|
||||
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class FlashFeiShuBindSucceedMsgView(TemplateView):
|
||||
template_name = 'flash_message_standalone.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
title = request.GET.get('title')
|
||||
msg = request.GET.get('msg')
|
||||
|
||||
context = {
|
||||
'title': title or _('Binding FeiShu successfully'),
|
||||
'messages': msg or _('Binding FeiShu successfully'),
|
||||
'interval': 5,
|
||||
'redirect_url': request.GET.get('redirect_url'),
|
||||
'auto_redirect': True,
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
|
||||
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class FlashFeiShuBindFailedMsgView(TemplateView):
|
||||
template_name = 'flash_message_standalone.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
title = request.GET.get('title')
|
||||
msg = request.GET.get('msg')
|
||||
|
||||
context = {
|
||||
'title': title or _('Binding FeiShu failed'),
|
||||
'messages': msg or _('Binding FeiShu failed'),
|
||||
'interval': 5,
|
||||
'redirect_url': request.GET.get('redirect_url'),
|
||||
'auto_redirect': True,
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
|
|
|
@ -5,6 +5,7 @@ from __future__ import unicode_literals
|
|||
import os
|
||||
import datetime
|
||||
|
||||
from django.templatetags.static import static
|
||||
from django.contrib.auth import login as auth_login, logout as auth_logout
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import reverse, redirect
|
||||
|
@ -28,7 +29,6 @@ from ..const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY
|
|||
from .. import mixins, errors
|
||||
from ..forms import get_user_login_form_cls
|
||||
|
||||
|
||||
__all__ = [
|
||||
'UserLoginView', 'UserLogoutView',
|
||||
'UserLoginGuardView', 'UserLoginWaitConfirmView',
|
||||
|
@ -66,6 +66,8 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
|||
return None
|
||||
|
||||
login_redirect = settings.LOGIN_REDIRECT_TO_BACKEND.lower()
|
||||
if login_redirect in ['direct']:
|
||||
return None
|
||||
if login_redirect in ['cas'] and cas_auth_url:
|
||||
auth_url = cas_auth_url
|
||||
elif login_redirect in ['openid', 'oidc'] and openid_auth_url:
|
||||
|
@ -109,19 +111,30 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
|||
self.request.session.delete_test_cookie()
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
self.check_user_auth(decrypt_passwd=True)
|
||||
self.check_user_auth(decrypt_passwd=True)
|
||||
except errors.AuthFailedError as e:
|
||||
form.add_error(None, e.msg)
|
||||
self.set_login_failed_mark()
|
||||
|
||||
form_cls = get_user_login_form_cls(captcha=True)
|
||||
new_form = form_cls(data=form.data)
|
||||
new_form._errors = form.errors
|
||||
context = self.get_context_data(form=new_form)
|
||||
self.request.session.set_test_cookie()
|
||||
return self.render_to_response(context)
|
||||
except (errors.PasswdTooSimple, errors.PasswordRequireResetError, errors.PasswdNeedUpdate) as e:
|
||||
except (
|
||||
errors.PasswdTooSimple,
|
||||
errors.PasswordRequireResetError,
|
||||
errors.PasswdNeedUpdate
|
||||
) as e:
|
||||
return redirect(e.url)
|
||||
except (
|
||||
errors.MFAUnsetError,
|
||||
errors.MFAFailedError,
|
||||
errors.BlockMFAError
|
||||
) as e:
|
||||
form.add_error('code', e.msg)
|
||||
return super().form_invalid(form)
|
||||
except errors.OTPRequiredError as e:
|
||||
return redirect(e.url)
|
||||
self.clear_rsa_key()
|
||||
return self.redirect_to_guard_view()
|
||||
|
@ -136,20 +149,56 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
|||
self.request.session[RSA_PRIVATE_KEY] = None
|
||||
self.request.session[RSA_PUBLIC_KEY] = None
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@staticmethod
|
||||
def get_support_auth_methods():
|
||||
auth_methods = [
|
||||
{
|
||||
'name': 'OpenID',
|
||||
'enabled': settings.AUTH_OPENID,
|
||||
'url': reverse('authentication:openid:login'),
|
||||
'logo': static('img/login_oidc_logo.png')
|
||||
},
|
||||
{
|
||||
'name': 'CAS',
|
||||
'enabled': settings.AUTH_CAS,
|
||||
'url': reverse('authentication:cas:cas-login'),
|
||||
'logo': static('img/login_cas_logo.png')
|
||||
},
|
||||
{
|
||||
'name': _('WeCom'),
|
||||
'enabled': settings.AUTH_WECOM,
|
||||
'url': reverse('authentication:wecom-qr-login'),
|
||||
'logo': static('img/login_wecom_logo.png')
|
||||
},
|
||||
{
|
||||
'name': _('DingTalk'),
|
||||
'enabled': settings.AUTH_DINGTALK,
|
||||
'url': reverse('authentication:dingtalk-qr-login'),
|
||||
'logo': static('img/login_dingtalk_logo.png')
|
||||
},
|
||||
{
|
||||
'name': _('FeiShu'),
|
||||
'enabled': settings.AUTH_FEISHU,
|
||||
'url': reverse('authentication:feishu-qr-login'),
|
||||
'logo': static('img/login_feishu_logo.png')
|
||||
}
|
||||
]
|
||||
return [method for method in auth_methods if method['enabled']]
|
||||
|
||||
@staticmethod
|
||||
def get_forgot_password_url():
|
||||
forgot_password_url = reverse('authentication:forgot-password')
|
||||
has_other_auth_backend = settings.AUTHENTICATION_BACKENDS[0] != settings.AUTH_BACKEND_MODEL
|
||||
if has_other_auth_backend and settings.FORGOT_PASSWORD_URL:
|
||||
forgot_password_url = settings.FORGOT_PASSWORD_URL
|
||||
return forgot_password_url
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'demo_mode': os.environ.get("DEMO_MODE"),
|
||||
'AUTH_OPENID': settings.AUTH_OPENID,
|
||||
'AUTH_CAS': settings.AUTH_CAS,
|
||||
'AUTH_WECOM': settings.AUTH_WECOM,
|
||||
'AUTH_DINGTALK': settings.AUTH_DINGTALK,
|
||||
'AUTH_FEISHU': settings.AUTH_FEISHU,
|
||||
'forgot_password_url': forgot_password_url
|
||||
'auth_methods': self.get_support_auth_methods(),
|
||||
'forgot_password_url': self.get_forgot_password_url(),
|
||||
'methods': self.get_user_mfa_methods(),
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
@ -254,7 +303,7 @@ class UserLogoutView(TemplateView):
|
|||
def get_context_data(self, **kwargs):
|
||||
context = {
|
||||
'title': _('Logout success'),
|
||||
'messages': _('Logout success, return login page'),
|
||||
'message': _('Logout success, return login page'),
|
||||
'interval': 3,
|
||||
'redirect_url': reverse('authentication:login'),
|
||||
'auto_redirect': True,
|
||||
|
|
|
@ -20,11 +20,11 @@ class UserLoginOtpView(mixins.AuthMixin, FormView):
|
|||
redirect_field_name = 'next'
|
||||
|
||||
def form_valid(self, form):
|
||||
otp_code = form.cleaned_data.get('code')
|
||||
code = form.cleaned_data.get('code')
|
||||
mfa_type = form.cleaned_data.get('mfa_type')
|
||||
|
||||
try:
|
||||
self.check_user_mfa(otp_code, mfa_type)
|
||||
self.check_user_mfa(code, mfa_type)
|
||||
return redirect_to_guard_view()
|
||||
except (errors.MFAFailedError, errors.BlockMFAError) as e:
|
||||
form.add_error('code', e.msg)
|
||||
|
@ -32,31 +32,11 @@ class UserLoginOtpView(mixins.AuthMixin, FormView):
|
|||
except Exception as e:
|
||||
logger.error(e)
|
||||
import traceback
|
||||
traceback.print_exception(e)
|
||||
traceback.print_exc()
|
||||
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
|
||||
methods = self.get_user_mfa_methods(user)
|
||||
kwargs.update({'methods': methods})
|
||||
return kwargs
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import urllib
|
||||
|
||||
from django.http.response import HttpResponseRedirect
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.generic import TemplateView
|
||||
from urllib.parse import urlencode
|
||||
from django.views import View
|
||||
from django.conf import settings
|
||||
from django.http.request import HttpRequest
|
||||
|
@ -15,7 +11,7 @@ from rest_framework.exceptions import APIException
|
|||
from users.views import UserVerifyPasswordView
|
||||
from users.utils import is_auth_password_time_valid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.utils import get_logger, FlashMessageUtil
|
||||
from common.utils.random import random_string
|
||||
from common.utils.django import reverse, get_object_or_none
|
||||
from common.message.backends.wecom import URL
|
||||
|
@ -39,7 +35,7 @@ class WeComQRMixin(PermissionsMixin, View):
|
|||
msg = e.detail['errmsg']
|
||||
except Exception:
|
||||
msg = _('WeCom Error, Please contact your system administrator')
|
||||
return self.get_failed_reponse(
|
||||
return self.get_failed_response(
|
||||
'/',
|
||||
_('WeCom Error'),
|
||||
msg
|
||||
|
@ -53,8 +49,8 @@ class WeComQRMixin(PermissionsMixin, View):
|
|||
return True
|
||||
|
||||
def get_verify_state_failed_response(self, redirect_uri):
|
||||
msg = _("You've been hacked")
|
||||
return self.get_failed_reponse(redirect_uri, msg, msg)
|
||||
msg = _("The system configuration is incorrect. Please contact your administrator")
|
||||
return self.get_failed_response(redirect_uri, msg, msg)
|
||||
|
||||
def get_qr_url(self, redirect_uri):
|
||||
state = random_string(16)
|
||||
|
@ -66,30 +62,32 @@ class WeComQRMixin(PermissionsMixin, View):
|
|||
'state': state,
|
||||
'redirect_uri': redirect_uri,
|
||||
}
|
||||
url = URL.QR_CONNECT + '?' + urllib.parse.urlencode(params)
|
||||
url = URL.QR_CONNECT + '?' + urlencode(params)
|
||||
return url
|
||||
|
||||
def get_success_reponse(self, redirect_url, title, msg):
|
||||
ok_flash_msg_url = reverse('authentication:wecom-bind-success-flash-msg')
|
||||
ok_flash_msg_url += '?' + urllib.parse.urlencode({
|
||||
'redirect_url': redirect_url,
|
||||
@staticmethod
|
||||
def get_success_response(redirect_url, title, msg):
|
||||
message_data = {
|
||||
'title': title,
|
||||
'msg': msg
|
||||
})
|
||||
return HttpResponseRedirect(ok_flash_msg_url)
|
||||
'message': msg,
|
||||
'interval': 5,
|
||||
'redirect_url': redirect_url,
|
||||
}
|
||||
return FlashMessageUtil.gen_and_redirect_to(message_data)
|
||||
|
||||
def get_failed_reponse(self, redirect_url, title, msg):
|
||||
failed_flash_msg_url = reverse('authentication:wecom-bind-failed-flash-msg')
|
||||
failed_flash_msg_url += '?' + urllib.parse.urlencode({
|
||||
'redirect_url': redirect_url,
|
||||
@staticmethod
|
||||
def get_failed_response(redirect_url, title, msg):
|
||||
message_data = {
|
||||
'title': title,
|
||||
'msg': msg
|
||||
})
|
||||
return HttpResponseRedirect(failed_flash_msg_url)
|
||||
'error': msg,
|
||||
'interval': 5,
|
||||
'redirect_url': redirect_url,
|
||||
}
|
||||
return FlashMessageUtil.gen_and_redirect_to(message_data)
|
||||
|
||||
def get_already_bound_response(self, redirect_url):
|
||||
msg = _('WeCom is already bound')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
response = self.get_failed_response(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
|
||||
|
@ -102,11 +100,11 @@ class WeComQRBindView(WeComQRMixin, View):
|
|||
|
||||
if not is_auth_password_time_valid(request.session):
|
||||
msg = _('Please verify your password first')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
response = self.get_failed_response(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
redirect_uri = reverse('authentication:wecom-qr-bind-callback', kwargs={'user_id': user.id}, external=True)
|
||||
redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url})
|
||||
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
|
||||
|
||||
url = self.get_qr_url(redirect_uri)
|
||||
return HttpResponseRedirect(url)
|
||||
|
@ -126,7 +124,7 @@ class WeComQRBindCallbackView(WeComQRMixin, View):
|
|||
if user is None:
|
||||
logger.error(f'WeComQR bind callback error, user_id invalid: user_id={user_id}')
|
||||
msg = _('Invalid user_id')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
response = self.get_failed_response(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
if user.wecom_id:
|
||||
|
@ -141,7 +139,7 @@ class WeComQRBindCallbackView(WeComQRMixin, View):
|
|||
wecom_userid, __ = wecom.get_user_id_by_code(code)
|
||||
if not wecom_userid:
|
||||
msg = _('WeCom query user failed')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
response = self.get_failed_response(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
try:
|
||||
|
@ -150,27 +148,24 @@ class WeComQRBindCallbackView(WeComQRMixin, View):
|
|||
except IntegrityError as e:
|
||||
if e.args[0] == 1062:
|
||||
msg = _('The WeCom is already bound to another user')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
response = self.get_failed_response(redirect_url, msg, msg)
|
||||
return response
|
||||
raise e
|
||||
|
||||
msg = _('Binding WeCom successfully')
|
||||
response = self.get_success_reponse(redirect_url, msg, msg)
|
||||
response = self.get_success_response(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
|
||||
class WeComEnableStartView(UserVerifyPasswordView):
|
||||
|
||||
def get_success_url(self):
|
||||
referer = self.request.META.get('HTTP_REFERER')
|
||||
redirect_url = self.request.GET.get("redirect_url")
|
||||
|
||||
success_url = reverse('authentication:wecom-qr-bind')
|
||||
|
||||
success_url += '?' + urllib.parse.urlencode({
|
||||
success_url += '?' + urlencode({
|
||||
'redirect_url': redirect_url or referer
|
||||
})
|
||||
|
||||
return success_url
|
||||
|
||||
|
||||
|
@ -181,7 +176,7 @@ class WeComQRLoginView(WeComQRMixin, View):
|
|||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
redirect_uri = reverse('authentication:wecom-qr-login-callback', external=True)
|
||||
redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url})
|
||||
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
|
||||
|
||||
url = self.get_qr_url(redirect_uri)
|
||||
return HttpResponseRedirect(url)
|
||||
|
@ -207,14 +202,14 @@ class WeComQRLoginCallbackView(AuthMixin, WeComQRMixin, View):
|
|||
if not wecom_userid:
|
||||
# 正常流程不会出这个错误,hack 行为
|
||||
msg = _('Failed to get user from WeCom')
|
||||
response = self.get_failed_reponse(login_url, title=msg, msg=msg)
|
||||
response = self.get_failed_response(login_url, title=msg, msg=msg)
|
||||
return response
|
||||
|
||||
user = get_object_or_none(User, wecom_id=wecom_userid)
|
||||
if user is None:
|
||||
title = _('WeCom is not bound')
|
||||
msg = _('Please login with a password and then bind the WeCom')
|
||||
response = self.get_failed_reponse(login_url, title=title, msg=msg)
|
||||
response = self.get_failed_response(login_url, title=title, msg=msg)
|
||||
return response
|
||||
|
||||
try:
|
||||
|
@ -222,43 +217,7 @@ class WeComQRLoginCallbackView(AuthMixin, WeComQRMixin, View):
|
|||
except errors.AuthFailedError as e:
|
||||
self.set_login_failed_mark()
|
||||
msg = e.msg
|
||||
response = self.get_failed_reponse(login_url, title=msg, msg=msg)
|
||||
response = self.get_failed_response(login_url, title=msg, msg=msg)
|
||||
return response
|
||||
|
||||
return self.redirect_to_guard_view()
|
||||
|
||||
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class FlashWeComBindSucceedMsgView(TemplateView):
|
||||
template_name = 'flash_message_standalone.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
title = request.GET.get('title')
|
||||
msg = request.GET.get('msg')
|
||||
|
||||
context = {
|
||||
'title': title or _('Binding WeCom successfully'),
|
||||
'messages': msg or _('Binding WeCom successfully'),
|
||||
'interval': 5,
|
||||
'redirect_url': request.GET.get('redirect_url'),
|
||||
'auto_redirect': True,
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
|
||||
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class FlashWeComBindFailedMsgView(TemplateView):
|
||||
template_name = 'flash_message_standalone.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
title = request.GET.get('title')
|
||||
msg = request.GET.get('msg')
|
||||
|
||||
context = {
|
||||
'title': title or _('Binding WeCom failed'),
|
||||
'messages': msg or _('Binding WeCom failed'),
|
||||
'interval': 5,
|
||||
'redirect_url': request.GET.get('redirect_url'),
|
||||
'auto_redirect': True,
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
|
|
|
@ -10,5 +10,6 @@ class CommonConfig(AppConfig):
|
|||
def ready(self):
|
||||
from . import signals_handlers
|
||||
from .signals import django_ready
|
||||
if 'migrate' not in sys.argv:
|
||||
django_ready.send(CommonConfig)
|
||||
if 'migrate' in sys.argv or 'compilemessages' in sys.argv:
|
||||
return
|
||||
django_ready.send(CommonConfig)
|
||||
|
|
|
@ -4,6 +4,7 @@ from __future__ import unicode_literals
|
|||
|
||||
from collections import OrderedDict
|
||||
import datetime
|
||||
from itertools import chain
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404
|
||||
|
@ -101,7 +102,13 @@ class SimpleMetadataWithFilters(SimpleMetadata):
|
|||
elif hasattr(view, 'get_filterset_fields'):
|
||||
fields = view.get_filterset_fields(request)
|
||||
elif hasattr(view, 'filterset_class'):
|
||||
fields = view.filterset_class.Meta.fields
|
||||
fields = list(view.filterset_class.Meta.fields) + \
|
||||
list(view.filterset_class.declared_filters.keys())
|
||||
|
||||
if hasattr(view, 'custom_filter_fields'):
|
||||
# 不能写 fields += view.custom_filter_fields
|
||||
# 会改变 view 的 filter_fields
|
||||
fields = list(fields) + list(view.custom_filter_fields)
|
||||
|
||||
if isinstance(fields, dict):
|
||||
fields = list(fields.keys())
|
||||
|
|
|
@ -14,7 +14,8 @@ def sign(secret, data):
|
|||
digest = hmac.HMAC(
|
||||
key=secret.encode('utf8'),
|
||||
msg=data.encode('utf8'),
|
||||
digestmod=hmac._hashlib.sha256).digest()
|
||||
digestmod=hmac._hashlib.sha256
|
||||
).digest()
|
||||
signature = base64.standard_b64encode(digest).decode('utf8')
|
||||
# signature = urllib.parse.quote(signature, safe='')
|
||||
# signature = signature.replace('+', '%20').replace('*', '%2A').replace('~', '%7E').replace('/', '%2F')
|
||||
|
@ -39,9 +40,9 @@ class DingTalkRequests(BaseRequest):
|
|||
invalid_token_errcodes = (ErrorCode.INVALID_TOKEN,)
|
||||
|
||||
def __init__(self, appid, appsecret, agentid, timeout=None):
|
||||
self._appid = appid
|
||||
self._appsecret = appsecret
|
||||
self._agentid = agentid
|
||||
self._appid = appid or ''
|
||||
self._appsecret = appsecret or ''
|
||||
self._agentid = agentid or ''
|
||||
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
|
@ -74,7 +75,7 @@ class DingTalkRequests(BaseRequest):
|
|||
def post(self, url, json=None, params=None,
|
||||
with_token=False, with_sign=False,
|
||||
check_errcode_is_0=True,
|
||||
**kwargs):
|
||||
**kwargs) -> dict:
|
||||
pass
|
||||
post = as_request(post)
|
||||
|
||||
|
@ -86,11 +87,10 @@ class DingTalkRequests(BaseRequest):
|
|||
|
||||
timestamp = str(int(time.time() * 1000))
|
||||
signature = sign(self._appsecret, timestamp)
|
||||
accessKey = self._appid
|
||||
|
||||
params['timestamp'] = timestamp
|
||||
params['signature'] = signature
|
||||
params['accessKey'] = accessKey
|
||||
params['accessKey'] = self._appid
|
||||
|
||||
def request(self, method, url,
|
||||
with_token=False, with_sign=False,
|
||||
|
@ -102,15 +102,16 @@ class DingTalkRequests(BaseRequest):
|
|||
|
||||
data = super().request(
|
||||
method, url, with_token=with_token,
|
||||
check_errcode_is_0=check_errcode_is_0, **kwargs)
|
||||
check_errcode_is_0=check_errcode_is_0, **kwargs
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
class DingTalk:
|
||||
def __init__(self, appid, appsecret, agentid, timeout=None):
|
||||
self._appid = appid
|
||||
self._appsecret = appsecret
|
||||
self._agentid = agentid
|
||||
self._appid = appid or ''
|
||||
self._appsecret = appsecret or ''
|
||||
self._agentid = agentid or ''
|
||||
|
||||
self._request = DingTalkRequests(
|
||||
appid=appid, appsecret=appsecret, agentid=agentid,
|
||||
|
|
|
@ -69,8 +69,8 @@ class FeiShu(RequestMixin):
|
|||
"""
|
||||
|
||||
def __init__(self, app_id, app_secret, timeout=None):
|
||||
self._app_id = app_id
|
||||
self._app_secret = app_secret
|
||||
self._app_id = app_id or ''
|
||||
self._app_secret = app_secret or ''
|
||||
|
||||
self._requests = FeishuRequests(
|
||||
app_id=app_id,
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import json
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
|
|
|
@ -8,12 +8,12 @@ from common.message.backends import exceptions as exce
|
|||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def digest(corpid, corpsecret):
|
||||
def digest(corp_id, corp_secret):
|
||||
md5 = hashlib.md5()
|
||||
md5.update(corpid.encode())
|
||||
md5.update(corpsecret.encode())
|
||||
digest = md5.hexdigest()
|
||||
return digest
|
||||
md5.update(corp_id.encode())
|
||||
md5.update(corp_secret.encode())
|
||||
dist = md5.hexdigest()
|
||||
return dist
|
||||
|
||||
|
||||
def update_values(default: dict, others: dict):
|
||||
|
|
|
@ -47,9 +47,9 @@ class WeComRequests(BaseRequest):
|
|||
invalid_token_errcodes = (ErrorCode.INVALID_TOKEN,)
|
||||
|
||||
def __init__(self, corpid, corpsecret, agentid, timeout=None):
|
||||
self._corpid = corpid
|
||||
self._corpsecret = corpsecret
|
||||
self._agentid = agentid
|
||||
self._corpid = corpid or ''
|
||||
self._corpsecret = corpsecret or ''
|
||||
self._agentid = agentid or ''
|
||||
|
||||
super().__init__(timeout=timeout)
|
||||
|
||||
|
@ -79,9 +79,9 @@ class WeCom(RequestMixin):
|
|||
"""
|
||||
|
||||
def __init__(self, corpid, corpsecret, agentid, timeout=None):
|
||||
self._corpid = corpid
|
||||
self._corpsecret = corpsecret
|
||||
self._agentid = agentid
|
||||
self._corpid = corpid or ''
|
||||
self._corpsecret = corpsecret or ''
|
||||
self._agentid = agentid or ''
|
||||
|
||||
self._requests = WeComRequests(
|
||||
corpid=corpid,
|
||||
|
|
|
@ -112,7 +112,8 @@ class ExtraFilterFieldsMixin:
|
|||
backends = list(chain(
|
||||
self.filter_backends,
|
||||
self.default_added_filters,
|
||||
self.extra_filter_backends))
|
||||
self.extra_filter_backends
|
||||
))
|
||||
return backends
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
|
|
|
@ -297,10 +297,10 @@ class CommonSerializerMixin(DynamicFieldsMixin, DefaultValueFieldsMixin):
|
|||
initial_data: dict
|
||||
|
||||
def get_initial_value(self, attr, default=None):
|
||||
if self.instance:
|
||||
return getattr(self.instance, attr, default)
|
||||
else:
|
||||
return self.initial_data.get(attr)
|
||||
value = self.initial_data.get(attr)
|
||||
if not value and self.instance:
|
||||
value = getattr(self.instance, attr, default)
|
||||
return value
|
||||
|
||||
|
||||
class CommonBulkSerializerMixin(BulkSerializerMixin, CommonSerializerMixin):
|
||||
|
|
|
@ -14,7 +14,7 @@ class IsValidUser(permissions.IsAuthenticated, permissions.BasePermission):
|
|||
|
||||
def has_permission(self, request, view):
|
||||
return super(IsValidUser, self).has_permission(request, view) \
|
||||
and request.user.is_valid
|
||||
and request.user.is_valid
|
||||
|
||||
|
||||
class IsAppUser(IsValidUser):
|
||||
|
@ -22,7 +22,7 @@ class IsAppUser(IsValidUser):
|
|||
|
||||
def has_permission(self, request, view):
|
||||
return super(IsAppUser, self).has_permission(request, view) \
|
||||
and request.user.is_app
|
||||
and request.user.is_app
|
||||
|
||||
|
||||
class IsSuperUser(IsValidUser):
|
||||
|
@ -36,7 +36,7 @@ class IsSuperUserOrAppUser(IsSuperUser):
|
|||
if request.user.is_anonymous:
|
||||
return False
|
||||
return super(IsSuperUserOrAppUser, self).has_permission(request, view) \
|
||||
or request.user.is_app
|
||||
or request.user.is_app
|
||||
|
||||
|
||||
class IsSuperAuditor(IsValidUser):
|
||||
|
@ -60,7 +60,7 @@ class IsOrgAdmin(IsValidUser):
|
|||
if not current_org:
|
||||
return False
|
||||
return super(IsOrgAdmin, self).has_permission(request, view) \
|
||||
and current_org.can_admin_by(request.user)
|
||||
and current_org.can_admin_by(request.user)
|
||||
|
||||
|
||||
class IsOrgAdminOrAppUser(IsValidUser):
|
||||
|
@ -72,7 +72,7 @@ class IsOrgAdminOrAppUser(IsValidUser):
|
|||
if request.user.is_anonymous:
|
||||
return False
|
||||
return super(IsOrgAdminOrAppUser, self).has_permission(request, view) \
|
||||
and (current_org.can_admin_by(request.user) or request.user.is_app)
|
||||
and (current_org.can_admin_by(request.user) or request.user.is_app)
|
||||
|
||||
|
||||
class IsOrgAdminOrAppUserOrUserReadonly(IsOrgAdminOrAppUser):
|
||||
|
|
|
@ -5,8 +5,8 @@ from .common import *
|
|||
from .django import *
|
||||
from .encode import *
|
||||
from .http import *
|
||||
from .ipip import *
|
||||
from .crypto import *
|
||||
from .random import *
|
||||
from .jumpserver import *
|
||||
from .ip import *
|
||||
from .geoip import *
|
||||
|
|
|
@ -10,7 +10,7 @@ from functools import wraps
|
|||
import time
|
||||
import ipaddress
|
||||
import psutil
|
||||
|
||||
from typing import Iterable
|
||||
|
||||
UUID_PATTERN = re.compile(r'\w{8}(-\w{4}){3}-\w{12}')
|
||||
ipip_db = None
|
||||
|
@ -275,7 +275,7 @@ class Time:
|
|||
last = timestamp
|
||||
|
||||
|
||||
def bulk_get(d, *keys, default=None):
|
||||
def bulk_get(d, keys, default=None):
|
||||
values = []
|
||||
for key in keys:
|
||||
values.append(d.get(key, default))
|
||||
|
@ -293,4 +293,3 @@ def unique(objects, key=None):
|
|||
if v not in seen:
|
||||
seen[v] = obj
|
||||
return list(seen.values())
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:860b4d38beff81667c64da41c026a7dd28c3c93a28ae61fefaa7c26875f35638
|
||||
size 73906864
|
|
@ -0,0 +1 @@
|
|||
from .utils import *
|
|
@ -0,0 +1,46 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import os
|
||||
import ipaddress
|
||||
|
||||
import geoip2.database
|
||||
from geoip2.errors import GeoIP2Error
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.conf import settings
|
||||
|
||||
__all__ = ['get_ip_city']
|
||||
reader = None
|
||||
|
||||
|
||||
def get_ip_city(ip):
|
||||
if not ip or '.' not in ip or not isinstance(ip, str):
|
||||
return _("Invalid ip")
|
||||
if ':' in ip:
|
||||
return 'IPv6'
|
||||
global reader
|
||||
if reader is None:
|
||||
path = os.path.join(os.path.dirname(__file__), 'GeoLite2-City.mmdb')
|
||||
reader = geoip2.database.Reader(path)
|
||||
|
||||
try:
|
||||
is_private = ipaddress.ip_address(ip.strip()).is_private
|
||||
if is_private:
|
||||
return _('LAN')
|
||||
except ValueError:
|
||||
return _("Invalid ip")
|
||||
|
||||
try:
|
||||
response = reader.city(ip)
|
||||
except GeoIP2Error:
|
||||
return _("Unknown ip")
|
||||
|
||||
names = response.city.names
|
||||
if not names:
|
||||
names = response.country.names
|
||||
|
||||
if 'en' in settings.LANGUAGE_CODE and 'en' in names:
|
||||
return names['en']
|
||||
elif 'zh-CN' in names:
|
||||
return names['zh-CN']
|
||||
return _("Unknown ip")
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from .utils import *
|
Binary file not shown.
|
@ -1,24 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import os
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
import ipdb
|
||||
|
||||
__all__ = ['get_ip_city']
|
||||
ipip_db = None
|
||||
|
||||
|
||||
def get_ip_city(ip):
|
||||
global ipip_db
|
||||
if not ip or not isinstance(ip, str):
|
||||
return _("Invalid ip")
|
||||
if ':' in ip:
|
||||
return 'IPv6'
|
||||
if ipip_db is None:
|
||||
ipip_db_path = os.path.join(os.path.dirname(__file__), 'ipipfree.ipdb')
|
||||
ipip_db = ipdb.City(ipip_db_path)
|
||||
info = list(set(ipip_db.find(ip, 'CN')))
|
||||
if '' in info:
|
||||
info.remove('')
|
||||
return ' '.join(info)
|
|
@ -1,5 +1,6 @@
|
|||
from django.core.cache import cache
|
||||
from django.shortcuts import reverse
|
||||
from django.shortcuts import redirect
|
||||
|
||||
from .random import random_string
|
||||
|
||||
|
@ -8,6 +9,17 @@ __all__ = ['FlashMessageUtil']
|
|||
|
||||
|
||||
class FlashMessageUtil:
|
||||
"""
|
||||
跳转到通用msg页面
|
||||
message_data: {
|
||||
'title': '',
|
||||
'message': '',
|
||||
'error': '',
|
||||
'redirect_url': '',
|
||||
'confirm_button': '',
|
||||
'cancel_url': ''
|
||||
}
|
||||
"""
|
||||
@staticmethod
|
||||
def get_key(code):
|
||||
key = 'MESSAGE_{}'.format(code)
|
||||
|
@ -29,3 +41,8 @@ class FlashMessageUtil:
|
|||
def gen_message_url(cls, message_data):
|
||||
code = cls.get_message_code(message_data)
|
||||
return reverse('common:flash-message') + f'?code={code}'
|
||||
|
||||
@classmethod
|
||||
def gen_and_redirect_to(cls, message_data):
|
||||
url = cls.gen_message_url(message_data)
|
||||
return redirect(url)
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
from common.utils.timezone import local_now
|
||||
|
||||
|
||||
def contains_time_period(time_periods):
|
||||
"""
|
||||
time_periods: [{"id": 1, "value": "00:00~07:30、10:00~13:00"}, {"id": 2, "value": "00:00~00:00"}]
|
||||
"""
|
||||
if not time_periods:
|
||||
return False
|
||||
|
||||
current_time = local_now().strftime('%H:%M')
|
||||
today_time_period = next(filter(lambda x: str(x['id']) == local_now().strftime("%w"), time_periods))
|
||||
for time in today_time_period['value'].split('、'):
|
||||
start, end = time.split('~')
|
||||
end = "24:00" if end == "00:00" else end
|
||||
if start <= current_time <= end:
|
||||
return True
|
||||
return False
|
|
@ -20,14 +20,14 @@ def as_current_tz(dt: datetime.datetime):
|
|||
return astimezone(dt, dj_timezone.get_current_timezone())
|
||||
|
||||
|
||||
def utcnow():
|
||||
def utc_now():
|
||||
return dj_timezone.now()
|
||||
|
||||
|
||||
def now():
|
||||
return as_current_tz(utcnow())
|
||||
def local_now():
|
||||
return as_current_tz(utc_now())
|
||||
|
||||
|
||||
_rest_dt_field = DateTimeField()
|
||||
dt_parser = _rest_dt_field.to_internal_value
|
||||
dt_formater = _rest_dt_field.to_representation
|
||||
dt_formatter = _rest_dt_field.to_representation
|
||||
|
|
|
@ -11,9 +11,12 @@ from rest_framework import serializers
|
|||
|
||||
from common.utils.strings import no_special_chars
|
||||
|
||||
|
||||
alphanumeric = RegexValidator(r'^[0-9a-zA-Z_@\-\.]*$', _('Special char not allowed'))
|
||||
|
||||
alphanumeric_re = re.compile(r'^[0-9a-zA-Z_@\-\.]*$')
|
||||
|
||||
alphanumeric_cn_re = re.compile(r'^[0-9a-zA-Z_@\-\.\u4E00-\u9FA5]*$')
|
||||
|
||||
|
||||
class ProjectUniqueValidator(UniqueTogetherValidator):
|
||||
def __call__(self, attrs, serializer):
|
||||
|
|
|
@ -20,21 +20,21 @@ class FlashMessageMsgView(TemplateView):
|
|||
if not message_data:
|
||||
return HttpResponse('Message code error')
|
||||
|
||||
title, message, redirect_url, confirm_button, cancel_url = bulk_get(
|
||||
message_data, 'title', 'message', 'redirect_url', 'confirm_button', 'cancel_url'
|
||||
)
|
||||
items = ('title', 'message', 'error', 'redirect_url', 'confirm_button', 'cancel_url')
|
||||
title, msg, error, redirect_url, confirm_btn, cancel_url = bulk_get(message_data, items)
|
||||
|
||||
interval = message_data.get('interval', 3)
|
||||
auto_redirect = message_data.get('auto_redirect', True)
|
||||
has_cancel = message_data.get('has_cancel', False)
|
||||
context = {
|
||||
'title': title,
|
||||
'messages': message,
|
||||
'message': msg,
|
||||
'error': error,
|
||||
'interval': interval,
|
||||
'redirect_url': redirect_url,
|
||||
'auto_redirect': auto_redirect,
|
||||
'confirm_button': confirm_button,
|
||||
'confirm_button': confirm_btn,
|
||||
'has_cancel': has_cancel,
|
||||
'cancel_url': cancel_url,
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
return self.render_to_response(context)
|
||||
|
|
|
@ -144,6 +144,10 @@ class Config(dict):
|
|||
|
||||
'GLOBAL_ORG_DISPLAY_NAME': '',
|
||||
'SITE_URL': 'http://localhost:8080',
|
||||
'USER_GUIDE_URL': '',
|
||||
'ANNOUNCEMENT_ENABLED': True,
|
||||
'ANNOUNCEMENT': {},
|
||||
|
||||
'CAPTCHA_TEST_MODE': None,
|
||||
'TOKEN_EXPIRATION': 3600 * 24,
|
||||
'DISPLAY_PER_PAGE': 25,
|
||||
|
@ -291,6 +295,7 @@ class Config(dict):
|
|||
'SECURITY_PASSWORD_LOWER_CASE': False,
|
||||
'SECURITY_PASSWORD_NUMBER': False,
|
||||
'SECURITY_PASSWORD_SPECIAL_CHAR': False,
|
||||
'SECURITY_MFA_IN_LOGIN_PAGE': False,
|
||||
'SECURITY_LOGIN_CHALLENGE_ENABLED': False,
|
||||
'SECURITY_LOGIN_CAPTCHA_ENABLED': True,
|
||||
'SECURITY_INSECURE_COMMAND': False,
|
||||
|
@ -301,7 +306,6 @@ class Config(dict):
|
|||
'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,
|
||||
|
|
|
@ -24,7 +24,6 @@ def jumpserver_processor(request):
|
|||
'SECURITY_MFA_VERIFY_TTL': settings.SECURITY_MFA_VERIFY_TTL,
|
||||
'FORCE_SCRIPT_NAME': settings.FORCE_SCRIPT_NAME,
|
||||
'SECURITY_VIEW_AUTH_NEED_MFA': settings.SECURITY_VIEW_AUTH_NEED_MFA,
|
||||
'LOGIN_CONFIRM_ENABLE': settings.LOGIN_CONFIRM_ENABLE,
|
||||
}
|
||||
return context
|
||||
|
||||
|
|
|
@ -126,7 +126,6 @@ FEISHU_APP_SECRET = CONFIG.FEISHU_APP_SECRET
|
|||
|
||||
# Other setting
|
||||
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
|
||||
LOGIN_CONFIRM_ENABLE = CONFIG.LOGIN_CONFIRM_ENABLE
|
||||
OTP_IN_RADIUS = CONFIG.OTP_IN_RADIUS
|
||||
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@ SECURITY_MFA_VERIFY_TTL = CONFIG.SECURITY_MFA_VERIFY_TTL
|
|||
SECURITY_VIEW_AUTH_NEED_MFA = CONFIG.SECURITY_VIEW_AUTH_NEED_MFA
|
||||
SECURITY_SERVICE_ACCOUNT_REGISTRATION = CONFIG.SECURITY_SERVICE_ACCOUNT_REGISTRATION
|
||||
SECURITY_LOGIN_CAPTCHA_ENABLED = CONFIG.SECURITY_LOGIN_CAPTCHA_ENABLED
|
||||
SECURITY_MFA_IN_LOGIN_PAGE = CONFIG.SECURITY_MFA_IN_LOGIN_PAGE
|
||||
SECURITY_LOGIN_CHALLENGE_ENABLED = CONFIG.SECURITY_LOGIN_CHALLENGE_ENABLED
|
||||
SECURITY_DATA_CRYPTO_ALGO = CONFIG.SECURITY_DATA_CRYPTO_ALGO
|
||||
SECURITY_INSECURE_COMMAND = CONFIG.SECURITY_INSECURE_COMMAND
|
||||
|
@ -149,3 +150,7 @@ 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
|
||||
|
||||
# 公告
|
||||
ANNOUNCEMENT_ENABLED = CONFIG.ANNOUNCEMENT_ENABLED
|
||||
ANNOUNCEMENT = CONFIG.ANNOUNCEMENT
|
||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
@ -6,4 +6,5 @@ class NotificationsConfig(AppConfig):
|
|||
|
||||
def ready(self):
|
||||
from . import signals_handler
|
||||
from . import notifications
|
||||
super().ready()
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
from typing import Iterable
|
||||
import traceback
|
||||
from html2text import HTML2Text
|
||||
from typing import Iterable
|
||||
from itertools import chain
|
||||
import time
|
||||
|
||||
from celery import shared_task
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.utils.timezone import now
|
||||
from common.utils.timezone import local_now
|
||||
from common.utils import lazyproperty
|
||||
from users.models import User
|
||||
from notifications.backends import BACKEND
|
||||
|
@ -17,6 +17,7 @@ __all__ = ('SystemMessage', 'UserMessage', 'system_msgs')
|
|||
|
||||
system_msgs = []
|
||||
user_msgs = []
|
||||
all_msgs = []
|
||||
|
||||
|
||||
class MessageType(type):
|
||||
|
@ -58,6 +59,7 @@ class Message(metaclass=MessageType):
|
|||
message_type_label: str
|
||||
category: str
|
||||
category_label: str
|
||||
text_msg_ignore_links = True
|
||||
|
||||
@classmethod
|
||||
def get_message_type(cls):
|
||||
|
@ -66,6 +68,10 @@ class Message(metaclass=MessageType):
|
|||
def publish_async(self):
|
||||
return publish_task.delay(self)
|
||||
|
||||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
raise NotImplementedError
|
||||
|
||||
def publish(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
@ -80,31 +86,46 @@ class Message(metaclass=MessageType):
|
|||
continue
|
||||
|
||||
get_msg_method = getattr(self, f'get_{backend}_msg', self.get_common_msg)
|
||||
|
||||
try:
|
||||
msg = get_msg_method()
|
||||
except NotImplementedError:
|
||||
continue
|
||||
|
||||
client = backend.client()
|
||||
|
||||
client.send_msg(users, **msg)
|
||||
except:
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
def send_test_msg(self):
|
||||
@classmethod
|
||||
def send_test_msg(cls, ding=True):
|
||||
msg = cls.gen_test_msg()
|
||||
if not msg:
|
||||
return
|
||||
|
||||
from users.models import User
|
||||
users = User.objects.filter(username='admin')
|
||||
self.send_msg(users, [])
|
||||
backends = []
|
||||
if ding:
|
||||
backends.append(BACKEND.DINGTALK)
|
||||
msg.send_msg(users, backends)
|
||||
|
||||
def get_common_msg(self) -> dict:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_text_msg(self) -> dict:
|
||||
return self.common_msg
|
||||
@staticmethod
|
||||
def get_common_msg() -> dict:
|
||||
return {
|
||||
'subject': '',
|
||||
'message': ''
|
||||
}
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
return self.common_msg
|
||||
return self.get_common_msg()
|
||||
|
||||
def get_text_msg(self) -> dict:
|
||||
h = HTML2Text()
|
||||
msg = self.get_html_msg()
|
||||
content = msg['message']
|
||||
h.ignore_links = self.text_msg_ignore_links
|
||||
msg['message'] = h.handle(content)
|
||||
return msg
|
||||
|
||||
@lazyproperty
|
||||
def common_msg(self) -> dict:
|
||||
|
@ -123,7 +144,8 @@ class Message(metaclass=MessageType):
|
|||
def get_dingtalk_msg(self) -> dict:
|
||||
# 钉钉相同的消息一天只能发一次,所以给所有消息添加基于时间的序号,使他们不相同
|
||||
message = self.text_msg['message']
|
||||
suffix = _('\nTime: {}').format(now())
|
||||
time = local_now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
suffix = '\n{}: {}'.format(_('Time'), time)
|
||||
|
||||
return {
|
||||
'subject': self.text_msg['subject'],
|
||||
|
@ -144,7 +166,25 @@ class Message(metaclass=MessageType):
|
|||
|
||||
def get_sms_msg(self) -> dict:
|
||||
return self.text_msg
|
||||
# --------------------------------------------------------------
|
||||
|
||||
@classmethod
|
||||
def test_all_messages(cls):
|
||||
def get_subclasses(cls):
|
||||
"""returns all subclasses of argument, cls"""
|
||||
if issubclass(cls, type):
|
||||
subclasses = cls.__subclasses__(cls)
|
||||
else:
|
||||
subclasses = cls.__subclasses__()
|
||||
for subclass in subclasses:
|
||||
subclasses.extend(get_subclasses(subclass))
|
||||
return subclasses
|
||||
|
||||
messages_cls = get_subclasses(cls)
|
||||
for _cls in messages_cls:
|
||||
try:
|
||||
msg = _cls.send_test_msg()
|
||||
except NotImplementedError:
|
||||
continue
|
||||
|
||||
|
||||
class SystemMessage(Message):
|
||||
|
@ -161,13 +201,16 @@ class SystemMessage(Message):
|
|||
*subscription.users.all(),
|
||||
*chain(*[g.users.all() for g in subscription.groups.all()])
|
||||
]
|
||||
|
||||
self.send_msg(users, receive_backends)
|
||||
|
||||
@classmethod
|
||||
def post_insert_to_db(cls, subscription: SystemMsgSubscription):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class UserMessage(Message):
|
||||
user: User
|
||||
|
@ -179,7 +222,9 @@ class UserMessage(Message):
|
|||
"""
|
||||
发送消息到每个用户配置的接收方式上
|
||||
"""
|
||||
|
||||
sub = UserMsgSubscription.objects.get(user=self.user)
|
||||
|
||||
self.send_msg([self.user], sub.receive_backends)
|
||||
|
||||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
raise NotImplementedError
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from django.db.models import F
|
||||
from django.db import transaction
|
||||
|
||||
from common.utils.timezone import now
|
||||
from common.utils.timezone import local_now
|
||||
from common.utils import get_logger
|
||||
from users.models import User
|
||||
from .models import SiteMessage as SiteMessageModel, SiteMessageUsers
|
||||
|
@ -88,7 +88,7 @@ class SiteMessageUtil:
|
|||
|
||||
for site_msg_user in site_msg_users:
|
||||
site_msg_user.has_read = True
|
||||
site_msg_user.read_at = now()
|
||||
site_msg_user.read_at = local_now()
|
||||
|
||||
SiteMessageUsers.objects.bulk_update(
|
||||
site_msg_users, fields=('has_read', 'read_at'))
|
||||
|
|
|
@ -11,7 +11,7 @@ from django_celery_beat.models import (
|
|||
PeriodicTask, IntervalSchedule, CrontabSchedule, PeriodicTasks
|
||||
)
|
||||
|
||||
from common.utils.timezone import now
|
||||
from common.utils.timezone import local_now
|
||||
from common.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
@ -52,7 +52,7 @@ def create_or_update_celery_periodic_tasks(tasks):
|
|||
interval = IntervalSchedule.objects.filter(**kwargs).first()
|
||||
if interval is None:
|
||||
interval = IntervalSchedule.objects.create(**kwargs)
|
||||
last_run_at = now()
|
||||
last_run_at = local_now()
|
||||
elif isinstance(detail.get("crontab"), str):
|
||||
try:
|
||||
minute, hour, day, month, week = detail["crontab"].split()
|
||||
|
|
|
@ -24,13 +24,6 @@ class ServerPerformanceMessage(SystemMessage):
|
|||
'message': self._msg
|
||||
}
|
||||
|
||||
def get_text_msg(self) -> dict:
|
||||
subject = self._msg[:80]
|
||||
return {
|
||||
'subject': subject.replace('<br>', '; '),
|
||||
'message': self._msg.replace('<br>', '\n')
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def post_insert_to_db(cls, subscription: SystemMsgSubscription):
|
||||
admins = User.objects.filter(role=User.ROLE.ADMIN)
|
||||
|
@ -38,6 +31,20 @@ class ServerPerformanceMessage(SystemMessage):
|
|||
subscription.receive_backends = [BACKEND.EMAIL]
|
||||
subscription.save()
|
||||
|
||||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
alarm_messages = []
|
||||
items_mapper = ServerPerformanceCheckUtil.items_mapper
|
||||
for item, data in items_mapper.items():
|
||||
msg = data['alarm_msg_format']
|
||||
max_threshold = data['max_threshold']
|
||||
value = 123
|
||||
msg = msg.format(max_threshold=max_threshold, value=value, name='Fake terminal')
|
||||
alarm_messages.append(msg)
|
||||
|
||||
msg = '<br>'.join(alarm_messages)
|
||||
return cls(msg)
|
||||
|
||||
|
||||
class ServerPerformanceCheckUtil(object):
|
||||
items_mapper = {
|
||||
|
@ -50,21 +57,21 @@ class ServerPerformanceCheckUtil(object):
|
|||
'default': 0,
|
||||
'max_threshold': 80,
|
||||
'alarm_msg_format': _(
|
||||
'[Disk] Disk used more than {max_threshold}%: => {value} ({name})'
|
||||
'Disk used more than {max_threshold}%: => {value} ({name})'
|
||||
)
|
||||
},
|
||||
'memory_used': {
|
||||
'default': 0,
|
||||
'max_threshold': 85,
|
||||
'alarm_msg_format': _(
|
||||
'[Memory] Memory used more than {max_threshold}%: => {value} ({name})'
|
||||
'Memory used more than {max_threshold}%: => {value} ({name})'
|
||||
),
|
||||
},
|
||||
'cpu_load': {
|
||||
'default': 0,
|
||||
'max_threshold': 5,
|
||||
'alarm_msg_format': _(
|
||||
'[CPU] CPU load more than {max_threshold}: => {value} ({name})'
|
||||
'CPU load more than {max_threshold}: => {value} ({name})'
|
||||
),
|
||||
},
|
||||
}
|
||||
|
|
|
@ -9,3 +9,4 @@ class PermsConfig(AppConfig):
|
|||
def ready(self):
|
||||
super().ready()
|
||||
from . import signals_handler
|
||||
from . import notifications
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
from common.utils import reverse as js_reverse
|
||||
from notifications.notifications import UserMessage
|
||||
|
||||
|
||||
class BasePermMsg(UserMessage):
|
||||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
return
|
||||
|
||||
|
||||
class PermedWillExpireUserMsg(BasePermMsg):
|
||||
def __init__(self, user, assets):
|
||||
super().__init__(user)
|
||||
self.assets = assets
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
subject = _("You permed assets is about to expire")
|
||||
context = {
|
||||
'name': self.user.name,
|
||||
'items': [str(asset) for asset in self.assets],
|
||||
'item_type': _("permed assets"),
|
||||
'show_help': True
|
||||
}
|
||||
message = render_to_string('perms/_msg_permed_items_expire.html', context)
|
||||
return {
|
||||
'subject': subject,
|
||||
'message': message
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
from users.models import User
|
||||
from assets.models import Asset
|
||||
user = User.objects.first()
|
||||
assets = Asset.objects.all()[:10]
|
||||
return cls(user, assets)
|
||||
|
||||
|
||||
class AssetPermsWillExpireForOrgAdminMsg(BasePermMsg):
|
||||
|
||||
def __init__(self, user, perms, org):
|
||||
super().__init__(user)
|
||||
self.perms = perms
|
||||
self.org = org
|
||||
|
||||
def get_items_with_url(self):
|
||||
items_with_url = []
|
||||
for perm in self.perms:
|
||||
url = js_reverse(
|
||||
'perms:asset-permission-detail',
|
||||
kwargs={'pk': perm.id}, external=True,
|
||||
api_to_ui=True
|
||||
) + f'?oid={perm.org_id}'
|
||||
items_with_url.append([perm.name, url])
|
||||
return items_with_url
|
||||
|
||||
def get_html_msg(self):
|
||||
items_with_url = self.get_items_with_url()
|
||||
subject = _("Asset permissions is about to expire")
|
||||
context = {
|
||||
'name': self.user.name,
|
||||
'items_with_url': items_with_url,
|
||||
'item_type': _('asset permissions of organization {}').format(self.org)
|
||||
}
|
||||
message = render_to_string('perms/_msg_item_permissions_expire.html', context)
|
||||
return {
|
||||
'subject': subject,
|
||||
'message': message
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
from users.models import User
|
||||
from perms.models import AssetPermission
|
||||
from orgs.models import Organization
|
||||
|
||||
user = User.objects.first()
|
||||
perms = AssetPermission.objects.all()[:10]
|
||||
org = Organization.objects.first()
|
||||
return cls(user, perms, org)
|
||||
|
||||
|
||||
class PermedAppsWillExpireUserMsg(BasePermMsg):
|
||||
def __init__(self, user, apps):
|
||||
super().__init__(user)
|
||||
self.apps = apps
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
subject = _("Your permed applications is about to expire")
|
||||
context = {
|
||||
'name': self.user.name,
|
||||
'item_type': _('permed applications'),
|
||||
'items': [str(app) for app in self.apps]
|
||||
}
|
||||
message = render_to_string('perms/_msg_permed_items_expire.html', context)
|
||||
return {
|
||||
'subject': subject,
|
||||
'message': message
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
from users.models import User
|
||||
from applications.models import Application
|
||||
|
||||
user = User.objects.first()
|
||||
apps = Application.objects.all()[:10]
|
||||
return cls(user, apps)
|
||||
|
||||
|
||||
class AppPermsWillExpireForOrgAdminMsg(BasePermMsg):
|
||||
def __init__(self, user, perms, org):
|
||||
super().__init__(user)
|
||||
self.perms = perms
|
||||
self.org = org
|
||||
|
||||
def get_items_with_url(self):
|
||||
items_with_url = []
|
||||
for perm in self.perms:
|
||||
url = js_reverse(
|
||||
'perms:application-permission-detail',
|
||||
kwargs={'pk': perm.id}, external=True,
|
||||
api_to_ui=True
|
||||
) + f'?oid={perm.org_id}'
|
||||
items_with_url.append([perm.name, url])
|
||||
return items_with_url
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
items = self.get_items_with_url()
|
||||
subject = _('Application permissions is about to expire')
|
||||
context = {
|
||||
'name': self.user.name,
|
||||
'item_type': _('application permissions of organization {}').format(self.org),
|
||||
'items_with_url': items
|
||||
}
|
||||
message = render_to_string('perms/_msg_item_permissions_expire.html', context)
|
||||
return {
|
||||
'subject': subject,
|
||||
'message': message
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
from users.models import User
|
||||
from perms.models import ApplicationPermission
|
||||
from orgs.models import Organization
|
||||
|
||||
user = User.objects.first()
|
||||
perms = ApplicationPermission.objects.all()[:10]
|
||||
org = Organization.objects.first()
|
||||
return cls(user, perms, org)
|
|
@ -1,14 +1,21 @@
|
|||
# ~*~ coding: utf-8 ~*~
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from datetime import timedelta
|
||||
from collections import defaultdict
|
||||
|
||||
from django.db.transaction import atomic
|
||||
from django.conf import settings
|
||||
from celery import shared_task
|
||||
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from common.utils import get_logger
|
||||
from common.utils.timezone import now, dt_formater, dt_parser
|
||||
from common.utils.timezone import local_now, dt_formatter, dt_parser
|
||||
from ops.celery.decorator import register_as_period_task
|
||||
from perms.models import AssetPermission
|
||||
from perms.notifications import (
|
||||
PermedWillExpireUserMsg, AssetPermsWillExpireForOrgAdminMsg,
|
||||
PermedAppsWillExpireUserMsg, AppPermsWillExpireForOrgAdminMsg
|
||||
)
|
||||
from perms.models import AssetPermission, ApplicationPermission
|
||||
from perms.utils.asset.user_permission import UserGrantedTreeRefreshController
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
@ -17,6 +24,7 @@ logger = get_logger(__file__)
|
|||
@register_as_period_task(interval=settings.PERM_EXPIRED_CHECK_PERIODIC)
|
||||
@shared_task()
|
||||
@atomic()
|
||||
@tmp_to_root_org()
|
||||
def check_asset_permission_expired():
|
||||
"""
|
||||
这里的任务要足够短,不要影响周期任务
|
||||
|
@ -25,10 +33,10 @@ def check_asset_permission_expired():
|
|||
|
||||
setting_name = 'last_asset_perm_expired_check'
|
||||
|
||||
end = now()
|
||||
end = local_now()
|
||||
default_start = end - timedelta(days=36000) # Long long ago in china
|
||||
|
||||
defaults = {'value': dt_formater(default_start)}
|
||||
defaults = {'value': dt_formatter(default_start)}
|
||||
setting, created = Setting.objects.get_or_create(
|
||||
name=setting_name, defaults=defaults
|
||||
)
|
||||
|
@ -36,7 +44,7 @@ def check_asset_permission_expired():
|
|||
start = default_start
|
||||
else:
|
||||
start = dt_parser(setting.value)
|
||||
setting.value = dt_formater(end)
|
||||
setting.value = dt_formatter(end)
|
||||
setting.save()
|
||||
|
||||
asset_perm_ids = AssetPermission.objects.filter(
|
||||
|
@ -45,3 +53,72 @@ def check_asset_permission_expired():
|
|||
asset_perm_ids = list(asset_perm_ids)
|
||||
logger.info(f'>>> checking {start} to {end} have {asset_perm_ids} expired')
|
||||
UserGrantedTreeRefreshController.add_need_refresh_by_asset_perm_ids_cross_orgs(asset_perm_ids)
|
||||
|
||||
|
||||
@register_as_period_task(crontab='0 10 * * *')
|
||||
@shared_task()
|
||||
@atomic()
|
||||
@tmp_to_root_org()
|
||||
def check_asset_permission_will_expired():
|
||||
start = local_now()
|
||||
end = start + timedelta(days=3)
|
||||
|
||||
user_asset_mapper = defaultdict(set)
|
||||
org_perm_mapper = defaultdict(set)
|
||||
|
||||
asset_perms = AssetPermission.objects.filter(
|
||||
date_expired__gte=start,
|
||||
date_expired__lte=end
|
||||
).distinct()
|
||||
|
||||
for asset_perm in asset_perms:
|
||||
# 资产授权按照组织分类
|
||||
org_perm_mapper[asset_perm.org].add(asset_perm)
|
||||
|
||||
# 计算每个用户即将过期的资产
|
||||
users = asset_perm.get_all_users()
|
||||
assets = asset_perm.get_all_assets()
|
||||
|
||||
for u in users:
|
||||
user_asset_mapper[u].update(assets)
|
||||
|
||||
for user, assets in user_asset_mapper.items():
|
||||
PermedWillExpireUserMsg(user, assets).publish_async()
|
||||
|
||||
for org, perms in org_perm_mapper.items():
|
||||
org_admins = org.admins.all()
|
||||
for org_admin in org_admins:
|
||||
AssetPermsWillExpireForOrgAdminMsg(org_admin, perms, org).publish_async()
|
||||
|
||||
|
||||
@register_as_period_task(crontab='0 10 * * *')
|
||||
@shared_task()
|
||||
@atomic()
|
||||
@tmp_to_root_org()
|
||||
def check_app_permission_will_expired():
|
||||
start = local_now()
|
||||
end = start + timedelta(days=3)
|
||||
|
||||
app_perms = ApplicationPermission.objects.filter(
|
||||
date_expired__gte=start,
|
||||
date_expired__lte=end
|
||||
).distinct()
|
||||
|
||||
user_app_mapper = defaultdict(set)
|
||||
org_perm_mapper = defaultdict(set)
|
||||
|
||||
for app_perm in app_perms:
|
||||
org_perm_mapper[app_perm.org].add(app_perm)
|
||||
|
||||
users = app_perm.get_all_users()
|
||||
apps = app_perm.applications.all()
|
||||
for u in users:
|
||||
user_app_mapper[u].update(apps)
|
||||
|
||||
for user, apps in user_app_mapper.items():
|
||||
PermedAppsWillExpireUserMsg(user, apps).publish_async()
|
||||
|
||||
for org, perms in org_perm_mapper.items():
|
||||
org_admins = org.admins.all()
|
||||
for org_admin in org_admins:
|
||||
AppPermsWillExpireForOrgAdminMsg(org_admin, perms, org).publish_async()
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
{% load i18n %}
|
||||
<p>
|
||||
{% trans 'Hello' %} {{ name }},
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{% blocktranslate %}
|
||||
The following {{ item_type }} will expire in 3 days
|
||||
{% endblocktranslate %}
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
{% for item, url in items_with_url %}
|
||||
<li><a href="{{ url }}">{{ item }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
|
@ -0,0 +1,24 @@
|
|||
{% load i18n %}
|
||||
<p>
|
||||
{% trans 'Hello' %} {{ name }},
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{% blocktranslate %}
|
||||
The following {{ item_type }} will expire in 3 days
|
||||
{% endblocktranslate %}
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
{% for item in items %}
|
||||
<li>{{ item }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<br />
|
||||
<p>
|
||||
---<br />
|
||||
<small>
|
||||
{% trans 'If you have any question, please contact the administrator' %}
|
||||
</small>
|
||||
</p>
|
|
@ -4,7 +4,7 @@ from rest_framework.exceptions import APIException
|
|||
from rest_framework import status
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from settings.models import Setting
|
||||
from django.conf import settings
|
||||
from common.permissions import IsSuperUser
|
||||
from common.message.backends.dingtalk import DingTalk
|
||||
|
||||
|
@ -19,24 +19,19 @@ class DingTalkTestingAPI(GenericAPIView):
|
|||
serializer = self.serializer_class(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
dingtalk_appkey = serializer.validated_data['DINGTALK_APPKEY']
|
||||
dingtalk_agentid = serializer.validated_data['DINGTALK_AGENTID']
|
||||
dingtalk_appsecret = serializer.validated_data.get('DINGTALK_APPSECRET')
|
||||
|
||||
if not dingtalk_appsecret:
|
||||
secret = Setting.objects.filter(name='DINGTALK_APPSECRET').first()
|
||||
if secret:
|
||||
dingtalk_appsecret = secret.cleaned_value
|
||||
|
||||
dingtalk_appsecret = dingtalk_appsecret or ''
|
||||
app_key = serializer.validated_data['DINGTALK_APPKEY']
|
||||
agent_id = serializer.validated_data['DINGTALK_AGENTID']
|
||||
app_secret = serializer.validated_data.get('DINGTALK_APPSECRET') \
|
||||
or settings.DINGTALK_APPSECRET \
|
||||
or ''
|
||||
|
||||
try:
|
||||
dingtalk = DingTalk(appid=dingtalk_appkey, appsecret=dingtalk_appsecret, agentid=dingtalk_agentid)
|
||||
dingtalk = DingTalk(appid=app_key, appsecret=app_secret, agentid=agent_id)
|
||||
dingtalk.send_text(['test'], 'test')
|
||||
return Response(status=status.HTTP_200_OK, data={'msg': _('Test success')})
|
||||
except APIException as e:
|
||||
try:
|
||||
if 'errmsg' in e.detail:
|
||||
error = e.detail['errmsg']
|
||||
except:
|
||||
else:
|
||||
error = e.detail
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': error})
|
||||
|
|
|
@ -9,6 +9,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from common.permissions import IsSuperUser
|
||||
from common.utils import get_logger
|
||||
from .. import serializers
|
||||
from django.conf import settings
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
@ -24,14 +25,15 @@ class MailTestingAPI(APIView):
|
|||
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']
|
||||
# 测试邮件时,邮件服务器信息从配置中获取
|
||||
email_host = settings.EMAIL_HOST
|
||||
email_port = settings.EMAIL_PORT
|
||||
email_host_user = settings.EMAIL_HOST_USER
|
||||
email_host_password = settings.EMAIL_HOST_PASSWORD
|
||||
email_from = serializer.validated_data.get('EMAIL_FROM')
|
||||
email_use_ssl = settings.EMAIL_USE_SSL
|
||||
email_use_tls = settings.EMAIL_USE_TLS
|
||||
email_recipient = serializer.validated_data.get('EMAIL_RECIPIENT')
|
||||
|
||||
# 设置 settings 的值,会导致动态配置在当前进程失效
|
||||
# for k, v in serializer.validated_data.items():
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue