mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-24 13:02:37 +00:00
Compare commits
109 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa08517bea | ||
|
|
f86d045c01 | ||
|
|
1a7fd58abf | ||
|
|
d808256e6a | ||
|
|
305a1b10ed | ||
|
|
8c277e8875 | ||
|
|
061b60ef59 | ||
|
|
c008115888 | ||
|
|
8d1fb84aaf | ||
|
|
43d61b5348 | ||
|
|
c26a786287 | ||
|
|
cb2bd0cf2c | ||
|
|
3048e6311b | ||
|
|
5e16b6387a | ||
|
|
93e1adf376 | ||
|
|
556bd3682e | ||
|
|
6bbbe312a2 | ||
|
|
1ac64db0ba | ||
|
|
fa54a98d6c | ||
|
|
31de9375e7 | ||
|
|
697270e3e6 | ||
|
|
56c324b04e | ||
|
|
984b94c874 | ||
|
|
50df7f1304 | ||
|
|
8e5833aef0 | ||
|
|
f20b465ddf | ||
|
|
409d254a2e | ||
|
|
e6d30fa77d | ||
|
|
b25404cac1 | ||
|
|
ef4cc5f646 | ||
|
|
f0dc519423 | ||
|
|
2cb6da3129 | ||
|
|
1819083a25 | ||
|
|
bdeec0d3cb | ||
|
|
8fc5c4cf9e | ||
|
|
89051b2c67 | ||
|
|
9123839b48 | ||
|
|
258c8a30d1 | ||
|
|
af75b5269c | ||
|
|
0a66693a41 | ||
|
|
7151201d58 | ||
|
|
51820f23bf | ||
|
|
8772cd8c71 | ||
|
|
60cb1f8136 | ||
|
|
5f1b7ff8f9 | ||
|
|
37b150bc04 | ||
|
|
1432fe1609 | ||
|
|
8ae98887ee | ||
|
|
24a1738e73 | ||
|
|
188c04c9a6 | ||
|
|
bb4da12366 | ||
|
|
382112ee33 | ||
|
|
3e69e6840b | ||
|
|
a82ed3e924 | ||
|
|
b347acd5ec | ||
|
|
ccd6b01020 | ||
|
|
831b67eae4 | ||
|
|
3ab634d88e | ||
|
|
867ad94a30 | ||
|
|
7d0a19635a | ||
|
|
4642804077 | ||
|
|
d405bae205 | ||
|
|
68841d1f15 | ||
|
|
4cad5affec | ||
|
|
2f8a07e665 | ||
|
|
78133b0c60 | ||
|
|
88d9078c43 | ||
|
|
5559f112db | ||
|
|
9a4b32cb3c | ||
|
|
ddf4b61c9f | ||
|
|
0eaaa7b4f6 | ||
|
|
09160fed5d | ||
|
|
18af5e8c4a | ||
|
|
1ed388459b | ||
|
|
2e944c6898 | ||
|
|
8409523fee | ||
|
|
16634907b4 | ||
|
|
cfa5de13ab | ||
|
|
28c8ec1fab | ||
|
|
a14ebc5f0f | ||
|
|
6af20d298d | ||
|
|
795d6e01dc | ||
|
|
acf8b5798b | ||
|
|
abcd12f645 | ||
|
|
30fe5214c7 | ||
|
|
708a87c903 | ||
|
|
6a30e0739d | ||
|
|
3951b8b080 | ||
|
|
c295f1451a | ||
|
|
c4a94876cc | ||
|
|
dcab934d9f | ||
|
|
4ecb0b760f | ||
|
|
b27b02eb9d | ||
|
|
70cf847cd9 | ||
|
|
2099baaaff | ||
|
|
b22aed0cc3 | ||
|
|
3e7f83d44e | ||
|
|
40f8b99242 | ||
|
|
9ff345747b | ||
|
|
9319c4748c | ||
|
|
e8b4ee5c40 | ||
|
|
429e838973 | ||
|
|
ee1aff243c | ||
|
|
ea7133dea0 | ||
|
|
e7229963bf | ||
|
|
0f7b41d177 | ||
|
|
c4146744e5 | ||
|
|
dc32224294 | ||
|
|
d07a230ba6 |
23
Dockerfile
23
Dockerfile
@@ -17,6 +17,7 @@ ARG DEPENDENCIES=" \
|
||||
libxmlsec1-dev \
|
||||
libxmlsec1-openssl \
|
||||
libaio-dev \
|
||||
openssh-client \
|
||||
sshpass"
|
||||
|
||||
ARG TOOLS=" \
|
||||
@@ -29,24 +30,22 @@ ARG TOOLS=" \
|
||||
redis-tools \
|
||||
telnet \
|
||||
vim \
|
||||
unzip \
|
||||
unzip \
|
||||
wget"
|
||||
|
||||
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 && sleep 1 && apt update \
|
||||
&& apt -y install ${BUILD_DEPENDENCIES} \
|
||||
&& apt -y install ${DEPENDENCIES} \
|
||||
&& apt -y install ${TOOLS} \
|
||||
RUN sed -i 's@http://.*.debian.org@http://mirrors.ustc.edu.cn@g' /etc/apt/sources.list \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${TOOLS} \
|
||||
&& localedef -c -f UTF-8 -i zh_CN zh_CN.UTF-8 \
|
||||
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& mkdir -p /root/.ssh/ \
|
||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
|
||||
&& sed -i "s@# alias l@alias l@g" ~/.bashrc \
|
||||
&& echo "set mouse-=a" > ~/.vimrc \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& mv /bin/sh /bin/sh.bak \
|
||||
&& ln -s /bin/bash /bin/sh
|
||||
&& echo "no" | dpkg-reconfigure dash \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG ORACLE_LIB_MAJOR=19
|
||||
@@ -65,9 +64,9 @@ RUN mkdir -p /opt/oracle/ \
|
||||
WORKDIR /tmp/build
|
||||
COPY ./requirements ./requirements
|
||||
|
||||
ARG PIP_MIRROR=https://mirrors.aliyun.com/pypi/simple/
|
||||
ARG PIP_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_MIRROR=$PIP_MIRROR
|
||||
ARG PIP_JMS_MIRROR=https://mirrors.aliyun.com/pypi/simple/
|
||||
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
|
||||
# 因为以 jms 或者 jumpserver 开头的 mirror 上可能没有
|
||||
RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \
|
||||
|
||||
16
README.md
16
README.md
@@ -16,7 +16,7 @@
|
||||
|
||||
|
||||
|
||||
JumpServer 是全球首款开源的堡垒机,使用 GPLv3 开源协议,是符合 4A 规范的运维安全审计系统。
|
||||
JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。
|
||||
|
||||
JumpServer 使用 Python 开发,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
|
||||
|
||||
@@ -95,11 +95,15 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
|
||||
### 案例研究
|
||||
|
||||
- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147)
|
||||
- [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882)
|
||||
- [携程 JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851)
|
||||
- [小红书的JumpServer堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516)
|
||||
- [JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732)
|
||||
- [腾讯海外游戏:基于JumpServer构建游戏安全运营能力](https://blog.fit2cloud.com/?p=3704)
|
||||
- [万华化学:通过JumpServer管理全球化分布式IT资产,并且实现与云管平台的联动](https://blog.fit2cloud.com/?p=3504)
|
||||
- [雪花啤酒:JumpServer堡垒机使用体会](https://blog.fit2cloud.com/?p=3412)
|
||||
- [顺丰科技:JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147)
|
||||
- [沐瞳游戏:通过JumpServer管控多项目分布式资产](https://blog.fit2cloud.com/?p=3213)
|
||||
- [携程:JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851)
|
||||
- [大智慧:JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882)
|
||||
- [小红书:的JumpServer堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516)
|
||||
- [中手游:JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732)
|
||||
- [中通快递:JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708)
|
||||
- [东方明珠:JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687)
|
||||
- [江苏农信:JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)
|
||||
|
||||
@@ -44,58 +44,29 @@ class LoginACL(BaseACL):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def action_reject(self):
|
||||
return self.action == self.ActionChoices.reject
|
||||
|
||||
@property
|
||||
def action_allow(self):
|
||||
return self.action == self.ActionChoices.allow
|
||||
def is_action(self, action):
|
||||
return self.action == action
|
||||
|
||||
@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
|
||||
def match(user, ip):
|
||||
acls = LoginACL.filter_acl(user)
|
||||
if not acls:
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def allow_user_to_login(user, ip):
|
||||
acl = LoginACL.filter_acl(user).exclude(
|
||||
action=LoginACL.ActionChoices.confirm
|
||||
).first()
|
||||
if not acl:
|
||||
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
|
||||
for acl in acls:
|
||||
if acl.is_action(LoginACL.ActionChoices.confirm) and not acl.reviewers.exists():
|
||||
continue
|
||||
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)
|
||||
if is_contain_ip and is_contain_time_period:
|
||||
# 满足条件,则返回
|
||||
return acl
|
||||
|
||||
def create_confirm_ticket(self, request):
|
||||
from tickets import const
|
||||
|
||||
@@ -16,7 +16,7 @@ from .. import const
|
||||
|
||||
__all__ = [
|
||||
'AppSerializer', 'MiniAppSerializer', 'AppSerializerMixin',
|
||||
'AppAccountSerializer', 'AppAccountSecretSerializer'
|
||||
'AppAccountSerializer', 'AppAccountSecretSerializer', 'AppAccountBackUpSerializer'
|
||||
]
|
||||
|
||||
|
||||
@@ -32,21 +32,23 @@ class AppSerializerMixin(serializers.Serializer):
|
||||
return instance
|
||||
|
||||
def get_attrs_serializer(self):
|
||||
default_serializer = serializers.Serializer(read_only=True)
|
||||
instance = self.app
|
||||
if instance:
|
||||
_type = instance.type
|
||||
_category = instance.category
|
||||
else:
|
||||
_type = self.context['request'].query_params.get('type')
|
||||
_category = self.context['request'].query_params.get('category')
|
||||
if _type:
|
||||
if isinstance(self, AppAccountSecretSerializer):
|
||||
serializer_class = type_secret_serializer_classes_mapping.get(_type)
|
||||
tp = getattr(self, 'tp', None)
|
||||
default_serializer = serializers.Serializer(read_only=True)
|
||||
if not tp:
|
||||
if instance:
|
||||
tp = instance.type
|
||||
category = instance.category
|
||||
else:
|
||||
serializer_class = type_serializer_classes_mapping.get(_type)
|
||||
elif _category:
|
||||
serializer_class = category_serializer_classes_mapping.get(_category)
|
||||
tp = self.context['request'].query_params.get('type')
|
||||
category = self.context['request'].query_params.get('category')
|
||||
if tp:
|
||||
if isinstance(self, AppAccountBackUpSerializer):
|
||||
serializer_class = type_secret_serializer_classes_mapping.get(tp)
|
||||
else:
|
||||
serializer_class = type_serializer_classes_mapping.get(tp)
|
||||
elif category:
|
||||
serializer_class = category_serializer_classes_mapping.get(category)
|
||||
else:
|
||||
serializer_class = default_serializer
|
||||
|
||||
@@ -154,11 +156,6 @@ class AppAccountSerializer(AppSerializerMixin, AuthSerializerMixin, BulkOrgResou
|
||||
|
||||
class AppAccountSecretSerializer(SecretReadableMixin, AppAccountSerializer):
|
||||
class Meta(AppAccountSerializer.Meta):
|
||||
fields_backup = [
|
||||
'id', 'app_display', 'attrs', 'username', 'password', 'private_key',
|
||||
'public_key', 'date_created', 'date_updated', 'version'
|
||||
]
|
||||
|
||||
extra_kwargs = {
|
||||
'password': {'write_only': False},
|
||||
'private_key': {'write_only': False},
|
||||
@@ -166,3 +163,22 @@ class AppAccountSecretSerializer(SecretReadableMixin, AppAccountSerializer):
|
||||
'app_display': {'label': _('Application display')},
|
||||
'systemuser_display': {'label': _('System User')}
|
||||
}
|
||||
|
||||
|
||||
class AppAccountBackUpSerializer(AppAccountSecretSerializer):
|
||||
class Meta(AppAccountSecretSerializer.Meta):
|
||||
fields = [
|
||||
'id', 'app_display', 'attrs', 'username', 'password', 'private_key',
|
||||
'public_key', 'date_created', 'date_updated', 'version'
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.tp = kwargs.pop('tp', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
return queryset
|
||||
|
||||
def to_representation(self, instance):
|
||||
return super(AppAccountSerializer, self).to_representation(instance)
|
||||
|
||||
@@ -13,3 +13,14 @@ class DBSerializer(serializers.Serializer):
|
||||
database = serializers.CharField(
|
||||
max_length=128, required=True, allow_null=True, label=_('Database')
|
||||
)
|
||||
use_ssl = serializers.BooleanField(default=False, label=_('Use SSL'))
|
||||
ca_cert = serializers.CharField(
|
||||
required=False, allow_null=True, label=_('CA certificate')
|
||||
)
|
||||
client_cert = serializers.CharField(
|
||||
required=False, allow_null=True, label=_('Client certificate file')
|
||||
)
|
||||
cert_key = serializers.CharField(
|
||||
required=False, allow_null=True, label=_('Certificate key file')
|
||||
)
|
||||
allow_invalid_cert = serializers.BooleanField(default=False, label=_('Allow invalid cert'))
|
||||
|
||||
@@ -6,7 +6,7 @@ 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.mixins.api import SuggestionMixin
|
||||
from common.mixins.api import SuggestionMixin, RenderToJsonMixin
|
||||
from users.models import User, UserGroup
|
||||
from users.serializers import UserSerializer, UserGroupSerializer
|
||||
from users.filters import UserFilter
|
||||
@@ -88,7 +88,7 @@ class AssetPlatformRetrieveApi(RetrieveAPIView):
|
||||
return asset.platform
|
||||
|
||||
|
||||
class AssetPlatformViewSet(ModelViewSet):
|
||||
class AssetPlatformViewSet(ModelViewSet, RenderToJsonMixin):
|
||||
queryset = Platform.objects.all()
|
||||
serializer_class = serializers.PlatformSerializer
|
||||
filterset_fields = ['name', 'base']
|
||||
|
||||
@@ -24,7 +24,7 @@ class SerializeToTreeNodeMixin:
|
||||
'title': _name(node),
|
||||
'pId': node.parent_key,
|
||||
'isParent': True,
|
||||
'open': node.is_org_root(),
|
||||
'open': True,
|
||||
'meta': {
|
||||
'data': {
|
||||
"id": node.id,
|
||||
|
||||
@@ -101,6 +101,8 @@ class NodeListAsTreeApi(generics.ListAPIView):
|
||||
|
||||
class NodeChildrenApi(generics.ListCreateAPIView):
|
||||
serializer_class = serializers.NodeSerializer
|
||||
search_fields = ('value',)
|
||||
|
||||
instance = None
|
||||
is_initial = False
|
||||
|
||||
@@ -179,8 +181,15 @@ class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi):
|
||||
"""
|
||||
model = Node
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
if not self.request.GET.get('search'):
|
||||
return queryset
|
||||
queryset = super().filter_queryset(queryset)
|
||||
queryset = self.model.get_ancestor_queryset(queryset)
|
||||
return queryset
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
nodes = self.get_queryset().order_by('value')
|
||||
nodes = self.filter_queryset(self.get_queryset()).order_by('value')
|
||||
nodes = self.serialize_nodes(nodes, with_asset_amount=True)
|
||||
assets = self.get_assets()
|
||||
data = [*nodes, *assets]
|
||||
|
||||
@@ -25,7 +25,6 @@ from orgs.mixins.models import OrgModelMixin, OrgManager
|
||||
from orgs.utils import get_current_org, tmp_to_org, tmp_to_root_org
|
||||
from orgs.models import Organization
|
||||
|
||||
|
||||
__all__ = ['Node', 'FamilyMixin', 'compute_parent_key', 'NodeQuerySet']
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -98,6 +97,14 @@ class FamilyMixin:
|
||||
q |= Q(key=self.key)
|
||||
return Node.objects.filter(q)
|
||||
|
||||
@classmethod
|
||||
def get_ancestor_queryset(cls, queryset, with_self=True):
|
||||
parent_keys = set()
|
||||
for i in queryset:
|
||||
parent_keys.update(set(i.get_ancestor_keys(with_self=with_self)))
|
||||
queryset = queryset.model.objects.filter(key__in=list(parent_keys)).distinct()
|
||||
return queryset
|
||||
|
||||
@property
|
||||
def children(self):
|
||||
return self.get_children(with_self=False)
|
||||
@@ -396,7 +403,7 @@ class NodeAllAssetsMappingMixin:
|
||||
mapping[ancestor_key].update(asset_ids)
|
||||
|
||||
t3 = time.time()
|
||||
logger.info('t1-t2(DB Query): {} s, t3-t2(Generate mapping): {} s'.format(t2-t1, t3-t2))
|
||||
logger.info('t1-t2(DB Query): {} s, t3-t2(Generate mapping): {} s'.format(t2 - t1, t3 - t2))
|
||||
return mapping
|
||||
|
||||
|
||||
|
||||
@@ -76,10 +76,6 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
|
||||
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
|
||||
class Meta(AccountSerializer.Meta):
|
||||
fields_backup = [
|
||||
'hostname', 'ip', 'platform', 'protocols', 'username', 'password',
|
||||
'private_key', 'public_key', 'date_created', 'date_updated', 'version'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'password': {'write_only': False},
|
||||
'private_key': {'write_only': False},
|
||||
@@ -88,6 +84,22 @@ class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
|
||||
}
|
||||
|
||||
|
||||
class AccountBackUpSerializer(AccountSecretSerializer):
|
||||
class Meta(AccountSecretSerializer.Meta):
|
||||
fields = [
|
||||
'id', 'hostname', 'ip', 'username', 'password',
|
||||
'private_key', 'public_key', 'date_created',
|
||||
'date_updated', 'version'
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
return queryset
|
||||
|
||||
def to_representation(self, instance):
|
||||
return super(AccountSerializer, self).to_representation(instance)
|
||||
|
||||
|
||||
class AccountTaskSerializer(serializers.Serializer):
|
||||
ACTION_CHOICES = (
|
||||
('test', 'test'),
|
||||
|
||||
@@ -189,6 +189,9 @@ class PlatformSerializer(serializers.ModelSerializer):
|
||||
'id', 'name', 'base', 'charset',
|
||||
'internal', 'meta', 'comment'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'internal': {'read_only': True},
|
||||
}
|
||||
|
||||
|
||||
class AssetSimpleSerializer(serializers.ModelSerializer):
|
||||
|
||||
@@ -4,15 +4,16 @@ from openpyxl import Workbook
|
||||
from collections import defaultdict, OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import F
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from assets.models import AuthBook
|
||||
from assets.serializers import AccountSecretSerializer
|
||||
from assets.models import AuthBook, SystemUser, Asset
|
||||
from assets.serializers import AccountBackUpSerializer
|
||||
from assets.notifications import AccountBackupExecutionTaskMsg
|
||||
from applications.models import Account
|
||||
from applications.models import Account, Application
|
||||
from applications.const import AppType
|
||||
from applications.serializers import AppAccountSecretSerializer
|
||||
from applications.serializers import AppAccountBackUpSerializer
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.utils.timezone import local_now_display
|
||||
@@ -38,7 +39,7 @@ class BaseAccountHandler:
|
||||
@classmethod
|
||||
def get_header_fields(cls, serializer: serializers.Serializer):
|
||||
try:
|
||||
backup_fields = getattr(serializer, 'Meta').fields_backup
|
||||
backup_fields = getattr(serializer, 'Meta').fields
|
||||
except AttributeError:
|
||||
backup_fields = serializer.fields.keys()
|
||||
header_fields = {}
|
||||
@@ -51,17 +52,41 @@ class BaseAccountHandler:
|
||||
header_fields[field] = str(v.label)
|
||||
return header_fields
|
||||
|
||||
@staticmethod
|
||||
def load_auth(tp, value, system_user):
|
||||
if value:
|
||||
return value
|
||||
if system_user:
|
||||
return getattr(system_user, tp, '')
|
||||
return ''
|
||||
|
||||
@classmethod
|
||||
def create_row(cls, account, serializer_cls, header_fields=None):
|
||||
serializer = serializer_cls(account)
|
||||
if not header_fields:
|
||||
header_fields = cls.get_header_fields(serializer)
|
||||
data = cls.unpack_data(serializer.data)
|
||||
def replace_auth(cls, account, system_user_dict):
|
||||
system_user = system_user_dict.get(account.systemuser_id)
|
||||
account.username = cls.load_auth('username', account.username, system_user)
|
||||
account.password = cls.load_auth('password', account.password, system_user)
|
||||
account.private_key = cls.load_auth('private_key', account.private_key, system_user)
|
||||
account.public_key = cls.load_auth('public_key', account.public_key, system_user)
|
||||
return account
|
||||
|
||||
@classmethod
|
||||
def create_row(cls, data, header_fields):
|
||||
data = cls.unpack_data(data)
|
||||
row_dict = {}
|
||||
for field, header_name in header_fields.items():
|
||||
row_dict[header_name] = str(data[field])
|
||||
row_dict[header_name] = str(data.get(field, field))
|
||||
return row_dict
|
||||
|
||||
@classmethod
|
||||
def add_rows(cls, data, header_fields, sheet):
|
||||
data_map = defaultdict(list)
|
||||
for i in data:
|
||||
row = cls.create_row(i, header_fields)
|
||||
if sheet not in data_map:
|
||||
data_map[sheet].append(list(row.keys()))
|
||||
data_map[sheet].append(list(row.values()))
|
||||
return data_map
|
||||
|
||||
|
||||
class AssetAccountHandler(BaseAccountHandler):
|
||||
@staticmethod
|
||||
@@ -72,22 +97,27 @@ class AssetAccountHandler(BaseAccountHandler):
|
||||
return filename
|
||||
|
||||
@classmethod
|
||||
def create_data_map(cls):
|
||||
data_map = defaultdict(list)
|
||||
def replace_account_info(cls, account, asset_dict, system_user_dict):
|
||||
asset = asset_dict.get(account.asset_id)
|
||||
account.ip = asset.ip if asset else ''
|
||||
account.hostname = asset.hostname if asset else ''
|
||||
account = cls.replace_auth(account, system_user_dict)
|
||||
return account
|
||||
|
||||
@classmethod
|
||||
def create_data_map(cls, system_user_dict):
|
||||
sheet_name = AuthBook._meta.verbose_name
|
||||
assets = Asset.objects.only('id', 'hostname', 'ip')
|
||||
asset_dict = {asset.id: asset for asset in assets}
|
||||
accounts = AuthBook.objects.all()
|
||||
if not accounts.exists():
|
||||
return
|
||||
|
||||
accounts = AuthBook.get_queryset().select_related('systemuser')
|
||||
if not accounts.first():
|
||||
return data_map
|
||||
|
||||
header_fields = cls.get_header_fields(AccountSecretSerializer(accounts.first()))
|
||||
header_fields = cls.get_header_fields(AccountBackUpSerializer(accounts.first()))
|
||||
for account in accounts:
|
||||
account.load_auth()
|
||||
row = cls.create_row(account, AccountSecretSerializer, header_fields)
|
||||
if sheet_name not in data_map:
|
||||
data_map[sheet_name].append(list(row.keys()))
|
||||
data_map[sheet_name].append(list(row.values()))
|
||||
|
||||
cls.replace_account_info(account, asset_dict, system_user_dict)
|
||||
data = AccountBackUpSerializer(accounts, many=True).data
|
||||
data_map = cls.add_rows(data, header_fields, sheet_name)
|
||||
logger.info('\n\033[33m- 共收集 {} 条资产账号\033[0m'.format(accounts.count()))
|
||||
return data_map
|
||||
|
||||
@@ -101,18 +131,36 @@ class AppAccountHandler(BaseAccountHandler):
|
||||
return filename
|
||||
|
||||
@classmethod
|
||||
def create_data_map(cls):
|
||||
data_map = defaultdict(list)
|
||||
accounts = Account.get_queryset().select_related('systemuser')
|
||||
for account in accounts:
|
||||
account.load_auth()
|
||||
app_type = account.type
|
||||
def replace_account_info(cls, account, app_dict, system_user_dict):
|
||||
app = app_dict.get(account.app_id)
|
||||
account.type = app.type if app else ''
|
||||
account.app_display = app.name if app else ''
|
||||
account.category = app.category if app else ''
|
||||
account = cls.replace_auth(account, system_user_dict)
|
||||
return account
|
||||
|
||||
@classmethod
|
||||
def create_data_map(cls, system_user_dict):
|
||||
apps = Application.objects.only('id', 'type', 'name', 'category')
|
||||
app_dict = {app.id: app for app in apps}
|
||||
qs = Account.objects.all().annotate(app_type=F('app__type'))
|
||||
if not qs.exists():
|
||||
return
|
||||
|
||||
account_type_map = defaultdict(list)
|
||||
for i in qs:
|
||||
account_type_map[i.app_type].append(i)
|
||||
data_map = {}
|
||||
for app_type, accounts in account_type_map.items():
|
||||
sheet_name = AppType.get_label(app_type)
|
||||
row = cls.create_row(account, AppAccountSecretSerializer)
|
||||
if sheet_name not in data_map:
|
||||
data_map[sheet_name].append(list(row.keys()))
|
||||
data_map[sheet_name].append(list(row.values()))
|
||||
logger.info('\n\033[33m- 共收集{}条应用账号\033[0m'.format(accounts.count()))
|
||||
header_fields = cls.get_header_fields(AppAccountBackUpSerializer(tp=app_type))
|
||||
if not accounts:
|
||||
continue
|
||||
for account in accounts:
|
||||
cls.replace_account_info(account, app_dict, system_user_dict)
|
||||
data = AppAccountBackUpSerializer(accounts, many=True, tp=app_type).data
|
||||
data_map.update(cls.add_rows(data, header_fields, sheet_name))
|
||||
logger.info('\n\033[33m- 共收集{}条应用账号\033[0m'.format(qs.count()))
|
||||
return data_map
|
||||
|
||||
|
||||
@@ -137,12 +185,16 @@ class AccountBackupHandler:
|
||||
# Print task start date
|
||||
time_start = time.time()
|
||||
files = []
|
||||
system_user_qs = SystemUser.objects.only(
|
||||
'id', 'username', 'password', 'private_key', 'public_key'
|
||||
)
|
||||
system_user_dict = {i.id: i for i in system_user_qs}
|
||||
for account_type in self.execution.types:
|
||||
handler = handler_map.get(account_type)
|
||||
if not handler:
|
||||
continue
|
||||
|
||||
data_map = handler.create_data_map()
|
||||
data_map = handler.create_data_map(system_user_dict)
|
||||
if not data_map:
|
||||
continue
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from common.api import CommonGenericViewSet
|
||||
from orgs.mixins.api import OrgGenericViewSet, OrgBulkModelViewSet, OrgRelationMixin
|
||||
from orgs.utils import current_org
|
||||
from ops.models import CommandExecution
|
||||
from . import filters
|
||||
from .models import FTPLog, UserLoginLog, OperateLog, PasswordChangeLog
|
||||
from .serializers import FTPLogSerializer, UserLoginLogSerializer, CommandExecutionSerializer
|
||||
from .serializers import OperateLogSerializer, PasswordChangeLogSerializer, CommandExecutionHostsRelationSerializer
|
||||
@@ -126,9 +127,7 @@ class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet):
|
||||
class CommandExecutionHostRelationViewSet(OrgRelationMixin, OrgBulkModelViewSet):
|
||||
serializer_class = CommandExecutionHostsRelationSerializer
|
||||
m2m_field = CommandExecution.hosts.field
|
||||
filterset_fields = [
|
||||
'id', 'asset', 'commandexecution'
|
||||
]
|
||||
filterset_class = filters.CommandExecutionFilter
|
||||
search_fields = ('asset__hostname', )
|
||||
http_method_names = ['options', 'get']
|
||||
rbac_perms = {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
from django.db.models import F, Value
|
||||
from django.db.models.functions import Concat
|
||||
from django_filters.rest_framework import CharFilter
|
||||
from rest_framework import filters
|
||||
from rest_framework.compat import coreapi, coreschema
|
||||
|
||||
from orgs.utils import current_org
|
||||
from ops.models import CommandExecution
|
||||
from common.drf.filters import BaseFilterSet
|
||||
|
||||
|
||||
__all__ = ['CurrentOrgMembersFilter']
|
||||
__all__ = ['CurrentOrgMembersFilter', 'CommandExecutionFilter']
|
||||
|
||||
|
||||
class CurrentOrgMembersFilter(filters.BaseFilterBackend):
|
||||
@@ -30,3 +34,22 @@ class CurrentOrgMembersFilter(filters.BaseFilterBackend):
|
||||
else:
|
||||
queryset = queryset.filter(user__in=self._get_user_list())
|
||||
return queryset
|
||||
|
||||
|
||||
class CommandExecutionFilter(BaseFilterSet):
|
||||
hostname_ip = CharFilter(method='filter_hostname_ip')
|
||||
|
||||
class Meta:
|
||||
model = CommandExecution.hosts.through
|
||||
fields = (
|
||||
'id', 'asset', 'commandexecution', 'hostname_ip'
|
||||
)
|
||||
|
||||
def filter_hostname_ip(self, queryset, name, value):
|
||||
queryset = queryset.annotate(
|
||||
hostname_ip=Concat(
|
||||
F('asset__hostname'), Value('('),
|
||||
F('asset__ip'), Value(')')
|
||||
)
|
||||
).filter(hostname_ip__icontains=value)
|
||||
return queryset
|
||||
|
||||
@@ -22,7 +22,6 @@ from ..serializers import (
|
||||
)
|
||||
from ..models import ConnectionToken
|
||||
|
||||
|
||||
__all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet']
|
||||
|
||||
|
||||
@@ -174,9 +173,8 @@ class ConnectionTokenMixin:
|
||||
rdp_options['remoteapplicationname:s'] = name
|
||||
else:
|
||||
name = '*'
|
||||
|
||||
filename = "{}-{}-jumpserver".format(token.user.username, name)
|
||||
filename = urllib.parse.quote(filename)
|
||||
prefix_name = f'{token.user.username}-{name}'
|
||||
filename = self.get_connect_filename(prefix_name)
|
||||
|
||||
content = ''
|
||||
for k, v in rdp_options.items():
|
||||
@@ -184,6 +182,15 @@ class ConnectionTokenMixin:
|
||||
|
||||
return filename, content
|
||||
|
||||
@staticmethod
|
||||
def get_connect_filename(prefix_name):
|
||||
prefix_name = prefix_name.replace('/', '_')
|
||||
prefix_name = prefix_name.replace('\\', '_')
|
||||
prefix_name = prefix_name.replace('.', '_')
|
||||
filename = f'{prefix_name}-jumpserver'
|
||||
filename = urllib.parse.quote(filename)
|
||||
return filename
|
||||
|
||||
def get_ssh_token(self, token: ConnectionToken):
|
||||
if token.asset:
|
||||
name = token.asset.hostname
|
||||
@@ -191,7 +198,8 @@ class ConnectionTokenMixin:
|
||||
name = token.application.name
|
||||
else:
|
||||
name = '*'
|
||||
filename = f'{token.user.username}-{name}-jumpserver'
|
||||
prefix_name = f'{token.user.username}-{name}'
|
||||
filename = self.get_connect_filename(prefix_name)
|
||||
|
||||
endpoint = self.get_smart_endpoint(
|
||||
protocol='ssh', asset=token.asset, application=token.application
|
||||
@@ -326,4 +334,3 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
|
||||
'msg': f'Token is renewed, date expired: {date_expired}'
|
||||
}
|
||||
return Response(data=data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ from rest_framework.permissions import AllowAny
|
||||
|
||||
from common.utils import get_logger
|
||||
from .. import errors, mixins
|
||||
from django.contrib.auth import logout as auth_logout
|
||||
|
||||
|
||||
__all__ = ['TicketStatusApi']
|
||||
logger = get_logger(__name__)
|
||||
@@ -17,7 +19,15 @@ class TicketStatusApi(mixins.AuthMixin, APIView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
try:
|
||||
self.check_user_login_confirm()
|
||||
self.request.session['auth_third_party_done'] = 1
|
||||
return Response({"msg": "ok"})
|
||||
except errors.LoginConfirmOtherError as e:
|
||||
reason = e.msg
|
||||
username = e.username
|
||||
self.send_auth_signal(success=False, username=username, reason=reason)
|
||||
# 若为三方登录,此时应退出登录
|
||||
auth_logout(request)
|
||||
return Response(e.as_data(), status=200)
|
||||
except errors.NeedMoreInfoError as e:
|
||||
return Response(e.as_data(), status=200)
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ class JMSBaseAuthBackend:
|
||||
if not allow:
|
||||
info = 'User {} skip authentication backend {}, because it not in {}'
|
||||
info = info.format(username, backend_name, ','.join(allowed_backend_names))
|
||||
logger.debug(info)
|
||||
logger.info(info)
|
||||
return allow
|
||||
|
||||
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
from django.urls import path
|
||||
import django_cas_ng.views
|
||||
|
||||
from .views import CASLoginView
|
||||
|
||||
urlpatterns = [
|
||||
path('login/', django_cas_ng.views.LoginView.as_view(), name='cas-login'),
|
||||
path('login/', CASLoginView.as_view(), name='cas-login'),
|
||||
path('logout/', django_cas_ng.views.LogoutView.as_view(), name='cas-logout'),
|
||||
path('callback/', django_cas_ng.views.CallbackView.as_view(), name='cas-proxy-callback'),
|
||||
]
|
||||
|
||||
15
apps/authentication/backends/cas/views.py
Normal file
15
apps/authentication/backends/cas/views.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from django_cas_ng.views import LoginView
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import HttpResponseRedirect
|
||||
|
||||
__all__ = ['LoginView']
|
||||
|
||||
|
||||
class CASLoginView(LoginView):
|
||||
def get(self, request):
|
||||
try:
|
||||
return super().get(request)
|
||||
except PermissionDenied:
|
||||
return HttpResponseRedirect('/')
|
||||
|
||||
|
||||
61
apps/authentication/backends/custom.py
Normal file
61
apps/authentication/backends/custom.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from django.conf import settings
|
||||
from django.utils.module_loading import import_string
|
||||
from common.utils import get_logger
|
||||
from django.contrib.auth import get_user_model
|
||||
from authentication.signals import user_auth_failed, user_auth_success
|
||||
|
||||
from .base import JMSModelBackend
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
custom_authenticate_method = None
|
||||
|
||||
if settings.AUTH_CUSTOM:
|
||||
""" 保证自定义认证方法在服务运行时不能被更改,只在第一次调用时加载一次 """
|
||||
try:
|
||||
custom_auth_method_path = 'data.auth.main.authenticate'
|
||||
custom_authenticate_method = import_string(custom_auth_method_path)
|
||||
except Exception as e:
|
||||
logger.warning('Import custom auth method failed: {}, Maybe not enabled'.format(e))
|
||||
|
||||
|
||||
class CustomAuthBackend(JMSModelBackend):
|
||||
|
||||
def is_enabled(self):
|
||||
return settings.AUTH_CUSTOM and callable(custom_authenticate_method)
|
||||
|
||||
@staticmethod
|
||||
def get_or_create_user_from_userinfo(userinfo: dict):
|
||||
username = userinfo['username']
|
||||
attrs = ['name', 'username', 'email', 'is_active']
|
||||
defaults = {attr: userinfo[attr] for attr in attrs}
|
||||
user, created = get_user_model().objects.get_or_create(
|
||||
username=username, defaults=defaults
|
||||
)
|
||||
return user, created
|
||||
|
||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||
try:
|
||||
userinfo: dict = custom_authenticate_method(
|
||||
username=username, password=password, **kwargs
|
||||
)
|
||||
user, created = self.get_or_create_user_from_userinfo(userinfo)
|
||||
except Exception as e:
|
||||
logger.error('Custom authenticate error: {}'.format(e))
|
||||
return None
|
||||
|
||||
if self.user_can_authenticate(user):
|
||||
logger.info(f'Custom authenticate success: {user.username}')
|
||||
user_auth_success.send(
|
||||
sender=self.__class__, request=request, user=user,
|
||||
backend=settings.AUTH_BACKEND_CUSTOM
|
||||
)
|
||||
return user
|
||||
else:
|
||||
logger.info(f'Custom authenticate failed: {user.username}')
|
||||
user_auth_failed.send(
|
||||
sender=self.__class__, request=request, username=user.username,
|
||||
reason=_('User invalid, disabled or expired'),
|
||||
backend=settings.AUTH_BACKEND_CUSTOM
|
||||
)
|
||||
return None
|
||||
4
apps/authentication/backends/oauth2/__init__.py
Normal file
4
apps/authentication/backends/oauth2/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from .backends import *
|
||||
161
apps/authentication/backends/oauth2/backends.py
Normal file
161
apps/authentication/backends/oauth2/backends.py
Normal file
@@ -0,0 +1,161 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import requests
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.http import urlencode
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
|
||||
from common.utils import get_logger
|
||||
from users.utils import construct_user_email
|
||||
from authentication.utils import build_absolute_uri
|
||||
from authentication.signals import user_auth_failed, user_auth_success
|
||||
from common.exceptions import JMSException
|
||||
|
||||
from .signals import (
|
||||
oauth2_create_or_update_user
|
||||
)
|
||||
from ..base import JMSModelBackend
|
||||
|
||||
|
||||
__all__ = ['OAuth2Backend']
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class OAuth2Backend(JMSModelBackend):
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
return settings.AUTH_OAUTH2
|
||||
|
||||
def get_or_create_user_from_userinfo(self, request, userinfo):
|
||||
log_prompt = "Get or Create user [OAuth2Backend]: {}"
|
||||
logger.debug(log_prompt.format('start'))
|
||||
|
||||
# Construct user attrs value
|
||||
user_attrs = {}
|
||||
for field, attr in settings.AUTH_OAUTH2_USER_ATTR_MAP.items():
|
||||
user_attrs[field] = userinfo.get(attr, '')
|
||||
|
||||
username = user_attrs.get('username')
|
||||
if not username:
|
||||
error_msg = 'username is missing'
|
||||
logger.error(log_prompt.format(error_msg))
|
||||
raise JMSException(error_msg)
|
||||
|
||||
email = user_attrs.get('email', '')
|
||||
email = construct_user_email(user_attrs.get('username'), email)
|
||||
user_attrs.update({'email': email})
|
||||
|
||||
logger.debug(log_prompt.format(user_attrs))
|
||||
user, created = get_user_model().objects.get_or_create(
|
||||
username=username, defaults=user_attrs
|
||||
)
|
||||
logger.debug(log_prompt.format("user: {}|created: {}".format(user, created)))
|
||||
logger.debug(log_prompt.format("Send signal => oauth2 create or update user"))
|
||||
oauth2_create_or_update_user.send(
|
||||
sender=self.__class__, request=request, user=user, created=created,
|
||||
attrs=user_attrs
|
||||
)
|
||||
return user, created
|
||||
|
||||
@staticmethod
|
||||
def get_response_data(response_data):
|
||||
if response_data.get('data') is not None:
|
||||
response_data = response_data['data']
|
||||
return response_data
|
||||
|
||||
@staticmethod
|
||||
def get_query_dict(response_data, query_dict):
|
||||
query_dict.update({
|
||||
'uid': response_data.get('uid', ''),
|
||||
'access_token': response_data.get('access_token', '')
|
||||
})
|
||||
return query_dict
|
||||
|
||||
def authenticate(self, request, code=None, **kwargs):
|
||||
log_prompt = "Process authenticate [OAuth2Backend]: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
if code is None:
|
||||
logger.error(log_prompt.format('code is missing'))
|
||||
return None
|
||||
|
||||
query_dict = {
|
||||
'client_id': settings.AUTH_OAUTH2_CLIENT_ID,
|
||||
'client_secret': settings.AUTH_OAUTH2_CLIENT_SECRET,
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': build_absolute_uri(
|
||||
request, path=reverse(settings.AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME)
|
||||
)
|
||||
}
|
||||
access_token_url = '{url}?{query}'.format(
|
||||
url=settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT, query=urlencode(query_dict)
|
||||
)
|
||||
token_method = settings.AUTH_OAUTH2_ACCESS_TOKEN_METHOD.lower()
|
||||
requests_func = getattr(requests, token_method, requests.get)
|
||||
logger.debug(log_prompt.format('Call the access token endpoint[method: %s]' % token_method))
|
||||
headers = {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
access_token_response = requests_func(access_token_url, headers=headers)
|
||||
try:
|
||||
access_token_response.raise_for_status()
|
||||
access_token_response_data = access_token_response.json()
|
||||
response_data = self.get_response_data(access_token_response_data)
|
||||
except Exception as e:
|
||||
error = "Json access token response error, access token response " \
|
||||
"content is: {}, error is: {}".format(access_token_response.content, str(e))
|
||||
logger.error(log_prompt.format(error))
|
||||
return None
|
||||
|
||||
query_dict = self.get_query_dict(response_data, query_dict)
|
||||
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'token {}'.format(response_data.get('access_token', ''))
|
||||
}
|
||||
|
||||
logger.debug(log_prompt.format('Get userinfo endpoint'))
|
||||
userinfo_url = '{url}?{query}'.format(
|
||||
url=settings.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT,
|
||||
query=urlencode(query_dict)
|
||||
)
|
||||
userinfo_response = requests.get(userinfo_url, headers=headers)
|
||||
try:
|
||||
userinfo_response.raise_for_status()
|
||||
userinfo_response_data = userinfo_response.json()
|
||||
if 'data' in userinfo_response_data:
|
||||
userinfo = userinfo_response_data['data']
|
||||
else:
|
||||
userinfo = userinfo_response_data
|
||||
except Exception as e:
|
||||
error = "Json userinfo response error, userinfo response " \
|
||||
"content is: {}, error is: {}".format(userinfo_response.content, str(e))
|
||||
logger.error(log_prompt.format(error))
|
||||
return None
|
||||
|
||||
try:
|
||||
logger.debug(log_prompt.format('Update or create oauth2 user'))
|
||||
user, created = self.get_or_create_user_from_userinfo(request, userinfo)
|
||||
except JMSException:
|
||||
return None
|
||||
|
||||
if self.user_can_authenticate(user):
|
||||
logger.debug(log_prompt.format('OAuth2 user login success'))
|
||||
logger.debug(log_prompt.format('Send signal => oauth2 user login success'))
|
||||
user_auth_success.send(
|
||||
sender=self.__class__, request=request, user=user,
|
||||
backend=settings.AUTH_BACKEND_OAUTH2
|
||||
)
|
||||
return user
|
||||
else:
|
||||
logger.debug(log_prompt.format('OAuth2 user login failed'))
|
||||
logger.debug(log_prompt.format('Send signal => oauth2 user login failed'))
|
||||
user_auth_failed.send(
|
||||
sender=self.__class__, request=request, username=user.username,
|
||||
reason=_('User invalid, disabled or expired'),
|
||||
backend=settings.AUTH_BACKEND_OAUTH2
|
||||
)
|
||||
return None
|
||||
7
apps/authentication/backends/oauth2/signals.py
Normal file
7
apps/authentication/backends/oauth2/signals.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.dispatch import Signal
|
||||
|
||||
|
||||
oauth2_create_or_update_user = Signal(
|
||||
providing_args=['request', 'user', 'created', 'name', 'username', 'email']
|
||||
)
|
||||
|
||||
12
apps/authentication/backends/oauth2/urls.py
Normal file
12
apps/authentication/backends/oauth2/urls.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('login/', views.OAuth2AuthRequestView.as_view(), name='login'),
|
||||
path('callback/', views.OAuth2AuthCallbackView.as_view(), name='login-callback'),
|
||||
path('logout/', views.OAuth2EndSessionView.as_view(), name='logout')
|
||||
]
|
||||
88
apps/authentication/backends/oauth2/views.py
Normal file
88
apps/authentication/backends/oauth2/views.py
Normal file
@@ -0,0 +1,88 @@
|
||||
from django.views import View
|
||||
from django.conf import settings
|
||||
from django.contrib import auth
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from django.utils.http import urlencode
|
||||
|
||||
from authentication.utils import build_absolute_uri
|
||||
from common.utils import get_logger
|
||||
from authentication.mixins import authenticate
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class OAuth2AuthRequestView(View):
|
||||
|
||||
def get(self, request):
|
||||
log_prompt = "Process OAuth2 GET requests: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
|
||||
query_dict = {
|
||||
'client_id': settings.AUTH_OAUTH2_CLIENT_ID, 'response_type': 'code',
|
||||
'scope': settings.AUTH_OAUTH2_SCOPE,
|
||||
'redirect_uri': build_absolute_uri(
|
||||
request, path=reverse(settings.AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME)
|
||||
)
|
||||
}
|
||||
|
||||
redirect_url = '{url}?{query}'.format(
|
||||
url=settings.AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT,
|
||||
query=urlencode(query_dict)
|
||||
)
|
||||
logger.debug(log_prompt.format('Redirect login url'))
|
||||
return HttpResponseRedirect(redirect_url)
|
||||
|
||||
|
||||
class OAuth2AuthCallbackView(View):
|
||||
http_method_names = ['get', ]
|
||||
|
||||
def get(self, request):
|
||||
""" Processes GET requests. """
|
||||
log_prompt = "Process GET requests [OAuth2AuthCallbackView]: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
callback_params = request.GET
|
||||
|
||||
if 'code' in callback_params:
|
||||
logger.debug(log_prompt.format('Process authenticate'))
|
||||
user = authenticate(code=callback_params['code'], request=request)
|
||||
if user and user.is_valid:
|
||||
logger.debug(log_prompt.format('Login: {}'.format(user)))
|
||||
auth.login(self.request, user)
|
||||
logger.debug(log_prompt.format('Redirect'))
|
||||
return HttpResponseRedirect(
|
||||
settings.AUTH_OAUTH2_AUTHENTICATION_REDIRECT_URI
|
||||
)
|
||||
|
||||
logger.debug(log_prompt.format('Redirect'))
|
||||
return HttpResponseRedirect(settings.AUTH_OAUTH2_AUTHENTICATION_FAILURE_REDIRECT_URI)
|
||||
|
||||
|
||||
class OAuth2EndSessionView(View):
|
||||
http_method_names = ['get', 'post', ]
|
||||
|
||||
def get(self, request):
|
||||
""" Processes GET requests. """
|
||||
log_prompt = "Process GET requests [OAuth2EndSessionView]: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
return self.post(request)
|
||||
|
||||
def post(self, request):
|
||||
""" Processes POST requests. """
|
||||
log_prompt = "Process POST requests [OAuth2EndSessionView]: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
|
||||
logout_url = settings.LOGOUT_REDIRECT_URL or '/'
|
||||
|
||||
# Log out the current user.
|
||||
if request.user.is_authenticated:
|
||||
logger.debug(log_prompt.format('Log out the current user: {}'.format(request.user)))
|
||||
auth.logout(request)
|
||||
|
||||
if settings.AUTH_OAUTH2_LOGOUT_COMPLETELY:
|
||||
logger.debug(log_prompt.format('Log out OAUTH2 platform user session synchronously'))
|
||||
next_url = settings.AUTH_OAUTH2_PROVIDER_END_SESSION_ENDPOINT
|
||||
return HttpResponseRedirect(next_url)
|
||||
|
||||
logger.debug(log_prompt.format('Redirect'))
|
||||
return HttpResponseRedirect(logout_url)
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import base64
|
||||
import requests
|
||||
|
||||
from rest_framework.exceptions import ParseError
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
@@ -18,14 +19,16 @@ from django.urls import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import get_logger
|
||||
from authentication.utils import build_absolute_uri_for_oidc
|
||||
from users.utils import construct_user_email
|
||||
|
||||
from ..base import JMSBaseAuthBackend
|
||||
from .utils import validate_and_return_id_token, build_absolute_uri
|
||||
from .utils import validate_and_return_id_token
|
||||
from .decorator import ssl_verification
|
||||
from .signals import (
|
||||
openid_create_or_update_user, openid_user_login_failed, openid_user_login_success
|
||||
openid_create_or_update_user
|
||||
)
|
||||
from authentication.signals import user_auth_success, user_auth_failed
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
@@ -127,7 +130,7 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
|
||||
token_payload = {
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': build_absolute_uri(
|
||||
'redirect_uri': build_absolute_uri_for_oidc(
|
||||
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
|
||||
)
|
||||
}
|
||||
@@ -211,14 +214,18 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
|
||||
if self.user_can_authenticate(user):
|
||||
logger.debug(log_prompt.format('OpenID user login success'))
|
||||
logger.debug(log_prompt.format('Send signal => openid user login success'))
|
||||
openid_user_login_success.send(sender=self.__class__, request=request, user=user)
|
||||
user_auth_success.send(
|
||||
sender=self.__class__, request=request, user=user,
|
||||
backend=settings.AUTH_BACKEND_OIDC_CODE
|
||||
)
|
||||
return user
|
||||
else:
|
||||
logger.debug(log_prompt.format('OpenID user login failed'))
|
||||
logger.debug(log_prompt.format('Send signal => openid user login failed'))
|
||||
openid_user_login_failed.send(
|
||||
user_auth_failed.send(
|
||||
sender=self.__class__, request=request, username=user.username,
|
||||
reason="User is invalid"
|
||||
reason="User is invalid", backend=settings.AUTH_BACKEND_OIDC_CODE
|
||||
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -269,8 +276,9 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend):
|
||||
"content is: {}, error is: {}".format(token_response.content, str(e))
|
||||
logger.debug(log_prompt.format(error))
|
||||
logger.debug(log_prompt.format('Send signal => openid user login failed'))
|
||||
openid_user_login_failed.send(
|
||||
sender=self.__class__, request=request, username=username, reason=error
|
||||
user_auth_failed.send(
|
||||
sender=self.__class__, request=request, username=username, reason=error,
|
||||
backend=settings.AUTH_BACKEND_OIDC_PASSWORD
|
||||
)
|
||||
return
|
||||
|
||||
@@ -297,8 +305,9 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend):
|
||||
"content is: {}, error is: {}".format(claims_response.content, str(e))
|
||||
logger.debug(log_prompt.format(error))
|
||||
logger.debug(log_prompt.format('Send signal => openid user login failed'))
|
||||
openid_user_login_failed.send(
|
||||
sender=self.__class__, request=request, username=username, reason=error
|
||||
user_auth_failed.send(
|
||||
sender=self.__class__, request=request, username=username, reason=error,
|
||||
backend=settings.AUTH_BACKEND_OIDC_PASSWORD
|
||||
)
|
||||
return
|
||||
|
||||
@@ -310,13 +319,16 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend):
|
||||
if self.user_can_authenticate(user):
|
||||
logger.debug(log_prompt.format('OpenID user login success'))
|
||||
logger.debug(log_prompt.format('Send signal => openid user login success'))
|
||||
openid_user_login_success.send(
|
||||
sender=self.__class__, request=request, user=user
|
||||
user_auth_success.send(
|
||||
sender=self.__class__, request=request, user=user,
|
||||
backend=settings.AUTH_BACKEND_OIDC_PASSWORD
|
||||
)
|
||||
return user
|
||||
else:
|
||||
logger.debug(log_prompt.format('OpenID user login failed'))
|
||||
logger.debug(log_prompt.format('Send signal => openid user login failed'))
|
||||
openid_user_login_failed.send(
|
||||
sender=self.__class__, request=request, username=username, reason="User is invalid"
|
||||
user_auth_failed.send(
|
||||
sender=self.__class__, request=request, username=username, reason="User is invalid",
|
||||
backend=settings.AUTH_BACKEND_OIDC_PASSWORD
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -13,6 +13,4 @@ from django.dispatch import Signal
|
||||
openid_create_or_update_user = Signal(
|
||||
providing_args=['request', 'user', 'created', 'name', 'username', 'email']
|
||||
)
|
||||
openid_user_login_success = Signal(providing_args=['request', 'user'])
|
||||
openid_user_login_failed = Signal(providing_args=['request', 'username', 'reason'])
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import datetime as dt
|
||||
from calendar import timegm
|
||||
from urllib.parse import urlparse, urljoin
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.utils.encoding import force_bytes, smart_bytes
|
||||
@@ -110,17 +110,3 @@ def _validate_claims(id_token, nonce=None, validate_nonce=True):
|
||||
raise SuspiciousOperation('Incorrect id_token: nonce')
|
||||
|
||||
logger.debug(log_prompt.format('End'))
|
||||
|
||||
|
||||
def build_absolute_uri(request, path=None):
|
||||
"""
|
||||
Build absolute redirect uri
|
||||
"""
|
||||
if path is None:
|
||||
path = '/'
|
||||
|
||||
if settings.BASE_SITE_URL:
|
||||
redirect_uri = urljoin(settings.BASE_SITE_URL, path)
|
||||
else:
|
||||
redirect_uri = request.build_absolute_uri(path)
|
||||
return redirect_uri
|
||||
|
||||
@@ -20,7 +20,8 @@ from django.utils.crypto import get_random_string
|
||||
from django.utils.http import is_safe_url, urlencode
|
||||
from django.views.generic import View
|
||||
|
||||
from .utils import get_logger, build_absolute_uri
|
||||
from authentication.utils import build_absolute_uri_for_oidc
|
||||
from .utils import get_logger
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
@@ -50,7 +51,7 @@ class OIDCAuthRequestView(View):
|
||||
'scope': settings.AUTH_OPENID_SCOPES,
|
||||
'response_type': 'code',
|
||||
'client_id': settings.AUTH_OPENID_CLIENT_ID,
|
||||
'redirect_uri': build_absolute_uri(
|
||||
'redirect_uri': build_absolute_uri_for_oidc(
|
||||
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
|
||||
)
|
||||
})
|
||||
@@ -216,7 +217,7 @@ class OIDCEndSessionView(View):
|
||||
""" Returns the end-session URL. """
|
||||
q = QueryDict(mutable=True)
|
||||
q[settings.AUTH_OPENID_PROVIDER_END_SESSION_REDIRECT_URI_PARAMETER] = \
|
||||
build_absolute_uri(self.request, path=settings.LOGOUT_REDIRECT_URL or '/')
|
||||
build_absolute_uri_for_oidc(self.request, path=settings.LOGOUT_REDIRECT_URL or '/')
|
||||
q[settings.AUTH_OPENID_PROVIDER_END_SESSION_ID_TOKEN_PARAMETER] = \
|
||||
self.request.session['oidc_auth_id_token']
|
||||
return '{}?{}'.format(settings.AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT, q.urlencode())
|
||||
|
||||
@@ -7,9 +7,9 @@ from django.db import transaction
|
||||
from common.utils import get_logger
|
||||
from authentication.errors import reason_choices, reason_user_invalid
|
||||
from .signals import (
|
||||
saml2_user_authenticated, saml2_user_authentication_failed,
|
||||
saml2_create_or_update_user
|
||||
)
|
||||
from authentication.signals import user_auth_failed, user_auth_success
|
||||
from ..base import JMSModelBackend
|
||||
|
||||
__all__ = ['SAML2Backend']
|
||||
@@ -39,7 +39,7 @@ class SAML2Backend(JMSModelBackend):
|
||||
return user, created
|
||||
|
||||
def authenticate(self, request, saml_user_data=None, **kwargs):
|
||||
log_prompt = "Process authenticate [SAML2AuthCodeBackend]: {}"
|
||||
log_prompt = "Process authenticate [SAML2Backend]: {}"
|
||||
logger.debug(log_prompt.format('Start'))
|
||||
if saml_user_data is None:
|
||||
logger.error(log_prompt.format('saml_user_data is missing'))
|
||||
@@ -48,21 +48,23 @@ class SAML2Backend(JMSModelBackend):
|
||||
logger.debug(log_prompt.format('saml data, {}'.format(saml_user_data)))
|
||||
username = saml_user_data.get('username')
|
||||
if not username:
|
||||
logger.debug(log_prompt.format('username is missing'))
|
||||
logger.warning(log_prompt.format('username is missing'))
|
||||
return None
|
||||
|
||||
user, created = self.get_or_create_from_saml_data(request, **saml_user_data)
|
||||
|
||||
if self.user_can_authenticate(user):
|
||||
logger.debug(log_prompt.format('SAML2 user login success'))
|
||||
saml2_user_authenticated.send(
|
||||
sender=self, request=request, user=user, created=created
|
||||
user_auth_success.send(
|
||||
sender=self.__class__, request=request, user=user, created=created,
|
||||
backend=settings.AUTH_BACKEND_SAML2
|
||||
)
|
||||
return user
|
||||
else:
|
||||
logger.debug(log_prompt.format('SAML2 user login failed'))
|
||||
saml2_user_authentication_failed.send(
|
||||
sender=self, request=request, username=username,
|
||||
reason=reason_choices.get(reason_user_invalid)
|
||||
user_auth_failed.send(
|
||||
sender=self.__class__, request=request, username=username,
|
||||
reason=reason_choices.get(reason_user_invalid),
|
||||
backend=settings.AUTH_BACKEND_SAML2
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -2,5 +2,3 @@ from django.dispatch import Signal
|
||||
|
||||
|
||||
saml2_create_or_update_user = Signal(providing_args=('user', 'created', 'request', 'attrs'))
|
||||
saml2_user_authenticated = Signal(providing_args=('user', 'created', 'request'))
|
||||
saml2_user_authentication_failed = Signal(providing_args=('request', 'username', 'reason'))
|
||||
|
||||
@@ -3,7 +3,7 @@ import copy
|
||||
from urllib import parse
|
||||
|
||||
from django.views import View
|
||||
from django.contrib import auth as auth
|
||||
from django.contrib import auth
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
@@ -12,12 +12,13 @@ class AuthFailedNeedLogMixin:
|
||||
username = ''
|
||||
request = None
|
||||
error = ''
|
||||
msg = ''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
post_auth_failed.send(
|
||||
sender=self.__class__, username=self.username,
|
||||
request=self.request, reason=self.error
|
||||
request=self.request, reason=self.msg
|
||||
)
|
||||
|
||||
|
||||
@@ -55,7 +56,8 @@ class BlockGlobalIpLoginError(AuthFailedError):
|
||||
error = 'block_global_ip_login'
|
||||
|
||||
def __init__(self, username, ip, **kwargs):
|
||||
self.msg = const.block_ip_login_msg.format(settings.SECURITY_LOGIN_IP_LIMIT_TIME)
|
||||
if not self.msg:
|
||||
self.msg = const.block_ip_login_msg.format(settings.SECURITY_LOGIN_IP_LIMIT_TIME)
|
||||
LoginIpBlockUtil(ip).set_block_if_need()
|
||||
super().__init__(username=username, ip=ip, **kwargs)
|
||||
|
||||
@@ -65,22 +67,21 @@ class CredentialError(
|
||||
BlockGlobalIpLoginError, AuthFailedError
|
||||
):
|
||||
def __init__(self, error, username, ip, request):
|
||||
super().__init__(error=error, username=username, ip=ip, request=request)
|
||||
util = LoginBlockUtil(username, ip)
|
||||
times_remainder = util.get_remainder_times()
|
||||
block_time = settings.SECURITY_LOGIN_LIMIT_TIME
|
||||
|
||||
if times_remainder < 1:
|
||||
self.msg = const.block_user_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
|
||||
return
|
||||
|
||||
default_msg = const.invalid_login_msg.format(
|
||||
times_try=times_remainder, block_time=block_time
|
||||
)
|
||||
if error == const.reason_password_failed:
|
||||
self.msg = default_msg
|
||||
else:
|
||||
self.msg = const.reason_choices.get(error, default_msg)
|
||||
default_msg = const.invalid_login_msg.format(
|
||||
times_try=times_remainder, block_time=block_time
|
||||
)
|
||||
if error == const.reason_password_failed:
|
||||
self.msg = default_msg
|
||||
else:
|
||||
self.msg = const.reason_choices.get(error, default_msg)
|
||||
# 先处理 msg 在 super,记录日志时原因才准确
|
||||
super().__init__(error=error, username=username, ip=ip, request=request)
|
||||
|
||||
|
||||
class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError):
|
||||
@@ -138,18 +139,11 @@ class ACLError(AuthFailedNeedLogMixin, AuthFailedError):
|
||||
}
|
||||
|
||||
|
||||
class LoginIPNotAllowed(ACLError):
|
||||
class LoginACLIPAndTimePeriodNotAllowed(ACLError):
|
||||
def __init__(self, username, request, **kwargs):
|
||||
self.username = username
|
||||
self.request = request
|
||||
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)
|
||||
super().__init__(_("Current IP and Time period is not allowed"), **kwargs)
|
||||
|
||||
|
||||
class MFACodeRequiredError(AuthFailedError):
|
||||
|
||||
@@ -69,10 +69,16 @@ class LoginConfirmWaitError(LoginConfirmBaseError):
|
||||
class LoginConfirmOtherError(LoginConfirmBaseError):
|
||||
error = 'login_confirm_error'
|
||||
|
||||
def __init__(self, ticket_id, status):
|
||||
def __init__(self, ticket_id, status, username):
|
||||
self.username = username
|
||||
msg = const.login_confirm_error_msg.format(status)
|
||||
super().__init__(ticket_id=ticket_id, msg=msg)
|
||||
|
||||
def as_data(self):
|
||||
ret = super().as_data()
|
||||
ret['data']['username'] = self.username
|
||||
return ret
|
||||
|
||||
|
||||
class PasswordTooSimple(NeedRedirectError):
|
||||
default_code = 'passwd_too_simple'
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import base64
|
||||
|
||||
from django.shortcuts import redirect, reverse
|
||||
from django.shortcuts import redirect, reverse, render
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from django.http import HttpResponse
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.contrib.auth import logout as auth_logout
|
||||
|
||||
from apps.authentication import mixins
|
||||
from common.utils import gen_key_pair
|
||||
from common.utils import get_request_ip
|
||||
from .signals import post_auth_failed
|
||||
|
||||
|
||||
class MFAMiddleware:
|
||||
@@ -13,6 +18,7 @@ class MFAMiddleware:
|
||||
这个 中间件 是用来全局拦截开启了 MFA 却没有认证的,如 OIDC, CAS,使用第三方库做的登录,直接 login 了,
|
||||
所以只能在 Middleware 中控制
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
@@ -42,6 +48,50 @@ class MFAMiddleware:
|
||||
return redirect(url)
|
||||
|
||||
|
||||
class ThirdPartyLoginMiddleware(mixins.AuthMixin):
|
||||
"""OpenID、CAS、SAML2登录规则设置验证"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
response = self.get_response(request)
|
||||
# 没有认证过,证明不是从 第三方 来的
|
||||
if request.user.is_anonymous:
|
||||
return response
|
||||
if not request.session.get('auth_third_party_required'):
|
||||
return response
|
||||
ip = get_request_ip(request)
|
||||
try:
|
||||
self.request = request
|
||||
self._check_login_acl(request.user, ip)
|
||||
except Exception as e:
|
||||
post_auth_failed.send(
|
||||
sender=self.__class__, username=request.user.username,
|
||||
request=self.request, reason=e.msg
|
||||
)
|
||||
auth_logout(request)
|
||||
context = {
|
||||
'title': _('Authentication failed'),
|
||||
'message': _('Authentication failed (before login check failed): {}').format(e),
|
||||
'interval': 10,
|
||||
'redirect_url': reverse('authentication:login'),
|
||||
'auto_redirect': True,
|
||||
}
|
||||
response = render(request, 'authentication/auth_fail_flash_message_standalone.html', context)
|
||||
else:
|
||||
if not self.request.session['auth_confirm_required']:
|
||||
return response
|
||||
guard_url = reverse('authentication:login-guard')
|
||||
args = request.META.get('QUERY_STRING', '')
|
||||
if args:
|
||||
guard_url = "%s?%s" % (guard_url, args)
|
||||
response = redirect(guard_url)
|
||||
finally:
|
||||
request.session.pop('auth_third_party_required', '')
|
||||
return response
|
||||
|
||||
|
||||
class SessionCookieMiddleware(MiddlewareMixin):
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -328,13 +328,59 @@ class AuthACLMixin:
|
||||
|
||||
def _check_login_acl(self, user, ip):
|
||||
# ACL 限制用户登录
|
||||
is_allowed, limit_type = LoginACL.allow_user_to_login(user, ip)
|
||||
if is_allowed:
|
||||
acl = LoginACL.match(user, ip)
|
||||
if not acl:
|
||||
return
|
||||
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)
|
||||
|
||||
acl: LoginACL
|
||||
if acl.is_action(acl.ActionChoices.allow):
|
||||
return
|
||||
|
||||
if acl.is_action(acl.ActionChoices.reject):
|
||||
raise errors.LoginACLIPAndTimePeriodNotAllowed(user.username, request=self.request)
|
||||
|
||||
if acl.is_action(acl.ActionChoices.confirm):
|
||||
self.request.session['auth_confirm_required'] = '1'
|
||||
self.request.session['auth_acl_id'] = str(acl.id)
|
||||
return
|
||||
|
||||
def check_user_login_confirm_if_need(self, user):
|
||||
if not self.request.session.get("auth_confirm_required"):
|
||||
return
|
||||
acl_id = self.request.session.get('auth_acl_id')
|
||||
logger.debug('Login confirm acl id: {}'.format(acl_id))
|
||||
if not acl_id:
|
||||
return
|
||||
acl = LoginACL.filter_acl(user).filter(id=acl_id).first()
|
||||
if not acl:
|
||||
return
|
||||
if not acl.is_action(acl.ActionChoices.confirm):
|
||||
return
|
||||
self.get_ticket_or_create(acl)
|
||||
self.check_user_login_confirm()
|
||||
|
||||
def get_ticket_or_create(self, acl):
|
||||
ticket = self.get_ticket()
|
||||
if not ticket or ticket.is_state(ticket.State.closed):
|
||||
ticket = acl.create_confirm_ticket(self.request)
|
||||
self.request.session['auth_ticket_id'] = str(ticket.id)
|
||||
return ticket
|
||||
|
||||
def check_user_login_confirm(self):
|
||||
ticket = self.get_ticket()
|
||||
if not ticket:
|
||||
raise errors.LoginConfirmOtherError('', "Not found")
|
||||
elif ticket.is_state(ticket.State.approved):
|
||||
self.request.session["auth_confirm_required"] = ''
|
||||
return
|
||||
elif ticket.is_status(ticket.Status.open):
|
||||
raise errors.LoginConfirmWaitError(ticket.id)
|
||||
else:
|
||||
# rejected, closed
|
||||
ticket_id = ticket.id
|
||||
status = ticket.get_state_display()
|
||||
username = ticket.applicant.username
|
||||
raise errors.LoginConfirmOtherError(ticket_id, status, username)
|
||||
|
||||
def get_ticket(self):
|
||||
from tickets.models import ApplyLoginTicket
|
||||
@@ -346,44 +392,6 @@ class AuthACLMixin:
|
||||
ticket = ApplyLoginTicket.all().filter(id=ticket_id).first()
|
||||
return ticket
|
||||
|
||||
def get_ticket_or_create(self, confirm_setting):
|
||||
ticket = self.get_ticket()
|
||||
if not ticket or ticket.is_status(ticket.Status.closed):
|
||||
ticket = confirm_setting.create_confirm_ticket(self.request)
|
||||
self.request.session['auth_ticket_id'] = str(ticket.id)
|
||||
return ticket
|
||||
|
||||
def check_user_login_confirm(self):
|
||||
ticket = self.get_ticket()
|
||||
if not ticket:
|
||||
raise errors.LoginConfirmOtherError('', "Not found")
|
||||
|
||||
if ticket.is_status(ticket.Status.open):
|
||||
raise errors.LoginConfirmWaitError(ticket.id)
|
||||
elif ticket.is_state(ticket.State.approved):
|
||||
self.request.session["auth_confirm"] = "1"
|
||||
return
|
||||
elif ticket.is_state(ticket.State.rejected):
|
||||
raise errors.LoginConfirmOtherError(
|
||||
ticket.id, ticket.get_state_display()
|
||||
)
|
||||
elif ticket.is_state(ticket.State.closed):
|
||||
raise errors.LoginConfirmOtherError(
|
||||
ticket.id, ticket.get_state_display()
|
||||
)
|
||||
else:
|
||||
raise errors.LoginConfirmOtherError(
|
||||
ticket.id, ticket.get_status_display()
|
||||
)
|
||||
|
||||
def check_user_login_confirm_if_need(self, user):
|
||||
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()
|
||||
|
||||
|
||||
class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPostCheckMixin):
|
||||
request = None
|
||||
@@ -482,7 +490,9 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost
|
||||
return self.check_user_auth(valid_data)
|
||||
|
||||
def clear_auth_mark(self):
|
||||
keys = ['auth_password', 'user_id', 'auth_confirm', 'auth_ticket_id']
|
||||
keys = [
|
||||
'auth_password', 'user_id', 'auth_confirm_required', 'auth_ticket_id', 'auth_acl_id'
|
||||
]
|
||||
for k in keys:
|
||||
self.request.session.pop(k, '')
|
||||
|
||||
|
||||
@@ -6,13 +6,8 @@ from django.core.cache import cache
|
||||
from django.dispatch import receiver
|
||||
from django_cas_ng.signals import cas_user_authenticated
|
||||
|
||||
from authentication.backends.oidc.signals import (
|
||||
openid_user_login_failed, openid_user_login_success
|
||||
)
|
||||
from authentication.backends.saml2.signals import (
|
||||
saml2_user_authenticated, saml2_user_authentication_failed
|
||||
)
|
||||
from .signals import post_auth_success, post_auth_failed
|
||||
from apps.jumpserver.settings.auth import AUTHENTICATION_BACKENDS_THIRD_PARTY
|
||||
from .signals import post_auth_success, post_auth_failed, user_auth_failed, user_auth_success
|
||||
|
||||
|
||||
@receiver(user_logged_in)
|
||||
@@ -25,7 +20,9 @@ def on_user_auth_login_success(sender, user, request, **kwargs):
|
||||
and user.mfa_enabled \
|
||||
and not request.session.get('auth_mfa'):
|
||||
request.session['auth_mfa_required'] = 1
|
||||
|
||||
if not request.session.get("auth_third_party_done") and \
|
||||
request.session.get('auth_backend') in AUTHENTICATION_BACKENDS_THIRD_PARTY:
|
||||
request.session['auth_third_party_required'] = 1
|
||||
# 单点登录,超过了自动退出
|
||||
if settings.USER_LOGIN_SINGLE_MACHINE_ENABLED:
|
||||
lock_key = 'single_machine_login_' + str(user.id)
|
||||
@@ -39,31 +36,19 @@ def on_user_auth_login_success(sender, user, request, **kwargs):
|
||||
request.session['auth_session_expiration_required'] = 1
|
||||
|
||||
|
||||
@receiver(openid_user_login_success)
|
||||
def on_oidc_user_login_success(sender, request, user, create=False, **kwargs):
|
||||
request.session['auth_backend'] = settings.AUTH_BACKEND_OIDC_CODE
|
||||
post_auth_success.send(sender, user=user, request=request)
|
||||
|
||||
|
||||
@receiver(openid_user_login_failed)
|
||||
def on_oidc_user_login_failed(sender, username, request, reason, **kwargs):
|
||||
request.session['auth_backend'] = settings.AUTH_BACKEND_OIDC_CODE
|
||||
post_auth_failed.send(sender, username=username, request=request, reason=reason)
|
||||
|
||||
|
||||
@receiver(cas_user_authenticated)
|
||||
def on_cas_user_login_success(sender, request, user, **kwargs):
|
||||
request.session['auth_backend'] = settings.AUTH_BACKEND_CAS
|
||||
post_auth_success.send(sender, user=user, request=request)
|
||||
|
||||
|
||||
@receiver(saml2_user_authenticated)
|
||||
def on_saml2_user_login_success(sender, request, user, **kwargs):
|
||||
request.session['auth_backend'] = settings.AUTH_BACKEND_SAML2
|
||||
@receiver(user_auth_success)
|
||||
def on_user_login_success(sender, request, user, backend, create=False, **kwargs):
|
||||
request.session['auth_backend'] = backend
|
||||
post_auth_success.send(sender, user=user, request=request)
|
||||
|
||||
|
||||
@receiver(saml2_user_authentication_failed)
|
||||
def on_saml2_user_login_failed(sender, request, username, reason, **kwargs):
|
||||
request.session['auth_backend'] = settings.AUTH_BACKEND_SAML2
|
||||
@receiver(user_auth_failed)
|
||||
def on_user_login_failed(sender, username, request, reason, backend, **kwargs):
|
||||
request.session['auth_backend'] = backend
|
||||
post_auth_failed.send(sender, username=username, request=request, reason=reason)
|
||||
|
||||
@@ -3,3 +3,7 @@ from django.dispatch import Signal
|
||||
|
||||
post_auth_success = Signal(providing_args=('user', 'request'))
|
||||
post_auth_failed = Signal(providing_args=('username', 'request', 'reason'))
|
||||
|
||||
|
||||
user_auth_success = Signal(providing_args=('user', 'request', 'backend', 'create'))
|
||||
user_auth_failed = Signal(providing_args=('username', 'request', 'reason', 'backend'))
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
{% extends '_base_only_content.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% block html_title %} {{ title }} {% endblock %}
|
||||
{% block title %} {{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.alert.alert-msg {
|
||||
background: #F5F5F7;
|
||||
}
|
||||
</style>
|
||||
<div>
|
||||
<p>
|
||||
<div class="alert alert-msg" id="messages">
|
||||
{% if error %}
|
||||
{{ error }}
|
||||
{% else %}
|
||||
{{ message|safe }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
{% if has_cancel %}
|
||||
<div class="col-sm-3">
|
||||
<a href="{{ cancel_url }}" class="btn btn-default block full-width m-b">
|
||||
{% trans 'Cancel' %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-sm-3">
|
||||
<a href="{{ redirect_url }}" class="btn btn-primary block full-width m-b">
|
||||
{% if confirm_button %}
|
||||
{{ confirm_button }}
|
||||
{% else %}
|
||||
{% trans 'Confirm' %}
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
var message = ''
|
||||
var time = '{{ interval }}'
|
||||
{% if error %}
|
||||
message = '{{ error }}'
|
||||
{% else %}
|
||||
message = '{{ message|safe }}'
|
||||
{% endif %}
|
||||
|
||||
function redirect_page() {
|
||||
if (time >= 0) {
|
||||
var msg = message + ', <b>' + time + '</b> ...';
|
||||
$('#messages').html(msg);
|
||||
time--;
|
||||
setTimeout(redirect_page, 1000);
|
||||
} else {
|
||||
window.location.href = "{{ redirect_url }}";
|
||||
}
|
||||
}
|
||||
{% if auto_redirect %}
|
||||
window.onload = redirect_page;
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -79,6 +79,9 @@ function doRequestAuth() {
|
||||
requestApi({
|
||||
url: url,
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-JMS-LOGIN-TYPE": "W"
|
||||
},
|
||||
success: function (data) {
|
||||
if (!data.error && data.msg === 'ok') {
|
||||
window.onbeforeunload = function(){};
|
||||
@@ -98,7 +101,7 @@ function doRequestAuth() {
|
||||
},
|
||||
error: function (text, data) {
|
||||
},
|
||||
flash_message: false
|
||||
flash_message: false, // 是否显示flash消息
|
||||
})
|
||||
}
|
||||
function initClipboard() {
|
||||
|
||||
@@ -56,9 +56,11 @@ urlpatterns = [
|
||||
path('profile/otp/disable/', users_view.UserOtpDisableView.as_view(),
|
||||
name='user-otp-disable'),
|
||||
|
||||
# openid
|
||||
# other authentication protocol
|
||||
path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')),
|
||||
path('openid/', include(('authentication.backends.oidc.urls', 'authentication'), namespace='openid')),
|
||||
path('saml2/', include(('authentication.backends.saml2.urls', 'authentication'), namespace='saml2')),
|
||||
path('oauth2/', include(('authentication.backends.oauth2.urls', 'authentication'), namespace='oauth2')),
|
||||
|
||||
path('captcha/', include('captcha.urls')),
|
||||
]
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import ipaddress
|
||||
from urllib.parse import urljoin, urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import validate_ip, get_ip_city, get_request_ip
|
||||
from common.utils import get_logger
|
||||
@@ -22,10 +25,34 @@ def check_different_city_login_if_need(user, request):
|
||||
else:
|
||||
city = get_ip_city(ip) or DEFAULT_CITY
|
||||
|
||||
city_white = ['LAN', ]
|
||||
if city not in city_white:
|
||||
city_white = [_('LAN'), 'LAN']
|
||||
is_private = ipaddress.ip_address(ip).is_private
|
||||
if not is_private:
|
||||
last_user_login = UserLoginLog.objects.exclude(city__in=city_white) \
|
||||
.filter(username=user.username, status=True).first()
|
||||
|
||||
if last_user_login and last_user_login.city != city:
|
||||
DifferentCityLoginMessage(user, ip, city).publish_async()
|
||||
|
||||
|
||||
def build_absolute_uri(request, path=None):
|
||||
""" Build absolute redirect """
|
||||
if path is None:
|
||||
path = '/'
|
||||
site_url = urlparse(settings.SITE_URL)
|
||||
scheme = site_url.scheme or request.scheme
|
||||
host = request.get_host()
|
||||
url = f'{scheme}://{host}'
|
||||
redirect_uri = urljoin(url, path)
|
||||
return redirect_uri
|
||||
|
||||
|
||||
def build_absolute_uri_for_oidc(request, path=None):
|
||||
""" Build absolute redirect uri for OIDC """
|
||||
if path is None:
|
||||
path = '/'
|
||||
if settings.BASE_SITE_URL:
|
||||
# OIDC 专用配置项
|
||||
redirect_uri = urljoin(settings.BASE_SITE_URL, path)
|
||||
return redirect_uri
|
||||
return build_absolute_uri(request, path=path)
|
||||
|
||||
@@ -21,7 +21,7 @@ from django.conf import settings
|
||||
from django.urls import reverse_lazy
|
||||
from django.contrib.auth import BACKEND_SESSION_KEY
|
||||
|
||||
from common.utils import FlashMessageUtil
|
||||
from common.utils import FlashMessageUtil, static_or_direct
|
||||
from users.utils import (
|
||||
redirect_user_first_login_or_index
|
||||
)
|
||||
@@ -39,8 +39,7 @@ class UserLoginContextMixin:
|
||||
get_user_mfa_context: Callable
|
||||
request: HttpRequest
|
||||
|
||||
@staticmethod
|
||||
def get_support_auth_methods():
|
||||
def get_support_auth_methods(self):
|
||||
auth_methods = [
|
||||
{
|
||||
'name': 'OpenID',
|
||||
@@ -63,6 +62,13 @@ class UserLoginContextMixin:
|
||||
'logo': static('img/login_saml2_logo.png'),
|
||||
'auto_redirect': True
|
||||
},
|
||||
{
|
||||
'name': settings.AUTH_OAUTH2_PROVIDER,
|
||||
'enabled': settings.AUTH_OAUTH2,
|
||||
'url': reverse('authentication:oauth2:login'),
|
||||
'logo': static_or_direct(settings.AUTH_OAUTH2_LOGO_PATH),
|
||||
'auto_redirect': True
|
||||
},
|
||||
{
|
||||
'name': _('WeCom'),
|
||||
'enabled': settings.AUTH_WECOM,
|
||||
@@ -324,6 +330,8 @@ class UserLogoutView(TemplateView):
|
||||
return settings.CAS_LOGOUT_URL_NAME
|
||||
elif 'saml2' in backend:
|
||||
return settings.SAML2_LOGOUT_URL_NAME
|
||||
elif 'oauth2' in backend:
|
||||
return settings.AUTH_OAUTH2_LOGOUT_URL_NAME
|
||||
return None
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
|
||||
@@ -10,5 +10,5 @@ celery_task_pre_key = "CELERY_"
|
||||
KEY_CACHE_RESOURCE_IDS = "RESOURCE_IDS_{}"
|
||||
|
||||
# AD User AccountDisable
|
||||
# https://blog.csdn.net/bytxl/article/details/17763975
|
||||
# https://docs.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties
|
||||
LDAP_AD_ACCOUNT_DISABLE = 2
|
||||
|
||||
@@ -67,7 +67,7 @@ class SimpleMetadataWithFilters(SimpleMetadata):
|
||||
|
||||
default = getattr(field, 'default', None)
|
||||
if default is not None and default != empty:
|
||||
if isinstance(default, (str, int, bool, datetime.datetime, list)):
|
||||
if isinstance(default, (str, int, bool, float, datetime.datetime, list)):
|
||||
field_info['default'] = default
|
||||
|
||||
for attr in self.attrs:
|
||||
|
||||
1
apps/common/hashers/__init__.py
Normal file
1
apps/common/hashers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .sm3 import PBKDF2SM3PasswordHasher
|
||||
23
apps/common/hashers/sm3.py
Normal file
23
apps/common/hashers/sm3.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from gmssl import sm3, func
|
||||
|
||||
from django.contrib.auth.hashers import PBKDF2PasswordHasher
|
||||
|
||||
|
||||
class Hasher:
|
||||
name = 'sm3'
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
||||
def hexdigest(self):
|
||||
return sm3.sm3_hash(func.bytes_to_list(self.key))
|
||||
|
||||
@staticmethod
|
||||
def hash(msg):
|
||||
return Hasher(msg)
|
||||
|
||||
|
||||
class PBKDF2SM3PasswordHasher(PBKDF2PasswordHasher):
|
||||
algorithm = "pbkdf2_sm3"
|
||||
digest = Hasher.hash
|
||||
|
||||
@@ -62,15 +62,22 @@ class UserConfirmation(permissions.BasePermission):
|
||||
|
||||
confirm_level = request.session.get('CONFIRM_LEVEL')
|
||||
confirm_time = request.session.get('CONFIRM_TIME')
|
||||
|
||||
ttl = self.get_ttl()
|
||||
if not confirm_level or not confirm_time or \
|
||||
confirm_level < self.min_level or \
|
||||
confirm_time < time.time() - self.ttl:
|
||||
confirm_time < time.time() - ttl:
|
||||
raise UserConfirmRequired(code=self.confirm_type)
|
||||
return True
|
||||
|
||||
def get_ttl(self):
|
||||
if self.confirm_type == ConfirmType.MFA:
|
||||
ttl = settings.SECURITY_MFA_VERIFY_TTL
|
||||
else:
|
||||
ttl = self.ttl
|
||||
return ttl
|
||||
|
||||
@classmethod
|
||||
def require(cls, confirm_type=ConfirmType.ReLogin, ttl=300):
|
||||
def require(cls, confirm_type=ConfirmType.ReLogin, ttl=60 * 5):
|
||||
min_level = ConfirmType.values.index(confirm_type) + 1
|
||||
name = 'UserConfirmationLevel{}TTL{}'.format(min_level, ttl)
|
||||
return type(name, (cls,), {'min_level': min_level, 'ttl': ttl, 'confirm_type': confirm_type})
|
||||
|
||||
0
apps/common/sdk/gm/__init__.py
Normal file
0
apps/common/sdk/gm/__init__.py
Normal file
7
apps/common/sdk/gm/piico/__init__.py
Normal file
7
apps/common/sdk/gm/piico/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from .device import Device
|
||||
|
||||
|
||||
def open_piico_device(driver_path) -> Device:
|
||||
d = Device()
|
||||
d.open(driver_path)
|
||||
return d
|
||||
59
apps/common/sdk/gm/piico/cipher.py
Normal file
59
apps/common/sdk/gm/piico/cipher.py
Normal file
@@ -0,0 +1,59 @@
|
||||
cipher_alg_id = {
|
||||
"sm4_ebc": 0x00000401,
|
||||
"sm4_cbc": 0x00000402,
|
||||
}
|
||||
|
||||
|
||||
class ECCCipher:
|
||||
def __init__(self, session, public_key, private_key):
|
||||
self._session = session
|
||||
self.public_key = public_key
|
||||
self.private_key = private_key
|
||||
|
||||
def encrypt(self, plain_text):
|
||||
return self._session.ecc_encrypt(self.public_key, plain_text, 0x00020800)
|
||||
|
||||
def decrypt(self, cipher_text):
|
||||
return self._session.ecc_decrypt(self.private_key, cipher_text, 0x00020800)
|
||||
|
||||
|
||||
class EBCCipher:
|
||||
|
||||
def __init__(self, session, key_val):
|
||||
self._session = session
|
||||
self._key = self.__get_key(key_val)
|
||||
self._alg = "sm4_ebc"
|
||||
self._iv = None
|
||||
|
||||
def __get_key(self, key_val):
|
||||
key_val = self.__padding(key_val)
|
||||
return self._session.import_key(key_val)
|
||||
|
||||
@staticmethod
|
||||
def __padding(val):
|
||||
# padding
|
||||
val = bytes(val)
|
||||
while len(val) % 16 != 0:
|
||||
val += b'\0'
|
||||
return val
|
||||
|
||||
def encrypt(self, plain_text):
|
||||
plain_text = self.__padding(plain_text)
|
||||
cipher_text = self._session.encrypt(plain_text, self._key, cipher_alg_id[self._alg], self._iv)
|
||||
return bytes(cipher_text)
|
||||
|
||||
def decrypt(self, cipher_text):
|
||||
plain_text = self._session.decrypt(cipher_text, self._key, cipher_alg_id[self._alg], self._iv)
|
||||
return bytes(plain_text)
|
||||
|
||||
def destroy(self):
|
||||
self._session.destroy_cipher_key(self._key)
|
||||
self._session.close()
|
||||
|
||||
|
||||
class CBCCipher(EBCCipher):
|
||||
|
||||
def __init__(self, session, key, iv):
|
||||
super().__init__(session, key)
|
||||
self._iv = iv
|
||||
self._alg = "sm4_cbc"
|
||||
70
apps/common/sdk/gm/piico/device.py
Normal file
70
apps/common/sdk/gm/piico/device.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from ctypes import *
|
||||
|
||||
from .exception import PiicoError
|
||||
from .session import Session
|
||||
from .cipher import *
|
||||
from .digest import *
|
||||
|
||||
|
||||
class Device:
|
||||
_driver = None
|
||||
__device = None
|
||||
|
||||
def open(self, driver_path="./libpiico_ccmu.so"):
|
||||
# load driver
|
||||
self.__load_driver(driver_path)
|
||||
# open device
|
||||
self.__open_device()
|
||||
|
||||
def close(self):
|
||||
if self.__device is None:
|
||||
raise Exception("device not turned on")
|
||||
ret = self._driver.SDF_CloseDevice(self.__device)
|
||||
if ret != 0:
|
||||
raise Exception("turn off device failed")
|
||||
self.__device = None
|
||||
|
||||
def new_session(self):
|
||||
session = c_void_p()
|
||||
ret = self._driver.SDF_OpenSession(self.__device, pointer(session))
|
||||
if ret != 0:
|
||||
raise Exception("create session failed")
|
||||
return Session(self._driver, session)
|
||||
|
||||
def generate_ecc_key_pair(self):
|
||||
session = self.new_session()
|
||||
return session.generate_ecc_key_pair(alg_id=0x00020200)
|
||||
|
||||
def generate_random(self, length=64):
|
||||
session = self.new_session()
|
||||
return session.generate_random(length)
|
||||
|
||||
def new_sm2_ecc_cipher(self, public_key, private_key):
|
||||
session = self.new_session()
|
||||
return ECCCipher(session, public_key, private_key)
|
||||
|
||||
def new_sm4_ebc_cipher(self, key_val):
|
||||
session = self.new_session()
|
||||
return EBCCipher(session, key_val)
|
||||
|
||||
def new_sm4_cbc_cipher(self, key_val, iv):
|
||||
session = self.new_session()
|
||||
return CBCCipher(session, key_val, iv)
|
||||
|
||||
def new_digest(self, mode="sm3"):
|
||||
session = self.new_session()
|
||||
return Digest(session, mode)
|
||||
|
||||
def __load_driver(self, path):
|
||||
# check driver status
|
||||
if self._driver is not None:
|
||||
raise Exception("already load driver")
|
||||
# load driver
|
||||
self._driver = cdll.LoadLibrary(path)
|
||||
|
||||
def __open_device(self):
|
||||
device = c_void_p()
|
||||
ret = self._driver.SDF_OpenDevice(pointer(device))
|
||||
if ret != 0:
|
||||
raise PiicoError("open piico device failed", ret)
|
||||
self.__device = device
|
||||
32
apps/common/sdk/gm/piico/digest.py
Normal file
32
apps/common/sdk/gm/piico/digest.py
Normal file
@@ -0,0 +1,32 @@
|
||||
hash_alg_id = {
|
||||
"sm3": 0x00000001,
|
||||
"sha1": 0x00000002,
|
||||
"sha256": 0x00000004,
|
||||
"sha512": 0x00000008,
|
||||
}
|
||||
|
||||
|
||||
class Digest:
|
||||
|
||||
def __init__(self, session, alg_name="sm3"):
|
||||
if hash_alg_id[alg_name] is None:
|
||||
raise Exception("unsupported hash alg {}".format(alg_name))
|
||||
|
||||
self._alg_name = alg_name
|
||||
self._session = session
|
||||
self.__init_hash()
|
||||
|
||||
def __init_hash(self):
|
||||
self._session.hash_init(hash_alg_id[self._alg_name])
|
||||
|
||||
def update(self, data):
|
||||
self._session.hash_update(data)
|
||||
|
||||
def final(self):
|
||||
return self._session.hash_final()
|
||||
|
||||
def reset(self):
|
||||
self.__init_hash()
|
||||
|
||||
def destroy(self):
|
||||
self._session.close()
|
||||
71
apps/common/sdk/gm/piico/ecc.py
Normal file
71
apps/common/sdk/gm/piico/ecc.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from ctypes import *
|
||||
|
||||
ECCref_MAX_BITS = 512
|
||||
ECCref_MAX_LEN = int((ECCref_MAX_BITS + 7) / 8)
|
||||
|
||||
|
||||
class EncodeMixin:
|
||||
def encode(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ECCrefPublicKey(Structure, EncodeMixin):
|
||||
_fields_ = [
|
||||
('bits', c_uint),
|
||||
('x', c_ubyte * ECCref_MAX_LEN),
|
||||
('y', c_ubyte * ECCref_MAX_LEN),
|
||||
]
|
||||
|
||||
def encode(self):
|
||||
return bytes([0x04]) + bytes(self.x[32:]) + bytes(self.y[32:])
|
||||
|
||||
|
||||
class ECCrefPrivateKey(Structure, EncodeMixin):
|
||||
_fields_ = [
|
||||
('bits', c_uint,),
|
||||
('K', c_ubyte * ECCref_MAX_LEN),
|
||||
]
|
||||
|
||||
def encode(self):
|
||||
return bytes(self.K[32:])
|
||||
|
||||
|
||||
class ECCCipherEncode(EncodeMixin):
|
||||
|
||||
def __init__(self):
|
||||
self.x = None
|
||||
self.y = None
|
||||
self.M = None
|
||||
self.C = None
|
||||
self.L = None
|
||||
|
||||
def encode(self):
|
||||
c1 = bytes(self.x[32:]) + bytes(self.y[32:])
|
||||
c2 = bytes(self.C[:self.L])
|
||||
c3 = bytes(self.M)
|
||||
return bytes([0x04]) + c1 + c2 + c3
|
||||
|
||||
|
||||
def new_ecc_cipher_cla(length):
|
||||
_cache = {}
|
||||
cla_name = "ECCCipher{}".format(length)
|
||||
if _cache.__contains__(cla_name):
|
||||
return _cache[cla_name]
|
||||
else:
|
||||
cla = type(cla_name, (Structure, ECCCipherEncode), {
|
||||
"_fields_": [
|
||||
('x', c_ubyte * ECCref_MAX_LEN),
|
||||
('y', c_ubyte * ECCref_MAX_LEN),
|
||||
('M', c_ubyte * 32),
|
||||
('L', c_uint),
|
||||
('C', c_ubyte * length)
|
||||
]
|
||||
})
|
||||
_cache[cla_name] = cla
|
||||
return cla
|
||||
|
||||
|
||||
class ECCKeyPair:
|
||||
def __init__(self, public_key, private_key):
|
||||
self.public_key = public_key
|
||||
self.private_key = private_key
|
||||
12
apps/common/sdk/gm/piico/exception.py
Normal file
12
apps/common/sdk/gm/piico/exception.py
Normal file
@@ -0,0 +1,12 @@
|
||||
class PiicoError(Exception):
|
||||
def __init__(self, msg, ret):
|
||||
super().__init__(self)
|
||||
self.__ret = ret
|
||||
self.__msg = msg
|
||||
|
||||
def __str__(self):
|
||||
return "piico error: {} return code: {}".format(self.__msg, self.hex_ret(self.__ret))
|
||||
|
||||
@staticmethod
|
||||
def hex_ret(ret):
|
||||
return hex(ret & ((1 << 32) - 1))
|
||||
36
apps/common/sdk/gm/piico/session.py
Normal file
36
apps/common/sdk/gm/piico/session.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from ctypes import *
|
||||
|
||||
from .ecc import ECCrefPublicKey, ECCrefPrivateKey, ECCKeyPair
|
||||
from .exception import PiicoError
|
||||
from .session_mixin import SM3Mixin, SM4Mixin, SM2Mixin
|
||||
|
||||
|
||||
class Session(SM2Mixin, SM3Mixin, SM4Mixin):
|
||||
def __init__(self, driver, session):
|
||||
super().__init__()
|
||||
self._session = session
|
||||
self._driver = driver
|
||||
|
||||
def get_device_info(self):
|
||||
pass
|
||||
|
||||
def generate_random(self, length=64):
|
||||
random_data = (c_ubyte * length)()
|
||||
ret = self._driver.SDF_GenerateRandom(self._session, c_int(length), random_data)
|
||||
if ret != 0:
|
||||
raise PiicoError("generate random error", ret)
|
||||
return bytes(random_data)
|
||||
|
||||
def generate_ecc_key_pair(self, alg_id):
|
||||
public_key = ECCrefPublicKey()
|
||||
private_key = ECCrefPrivateKey()
|
||||
ret = self._driver.SDF_GenerateKeyPair_ECC(self._session, c_int(alg_id), c_int(256), pointer(public_key),
|
||||
pointer(private_key))
|
||||
if ret != 0:
|
||||
raise PiicoError("generate ecc key pair failed", ret)
|
||||
return ECCKeyPair(public_key.encode(), private_key.encode())
|
||||
|
||||
def close(self):
|
||||
ret = self._driver.SDF_CloseSession(self._session)
|
||||
if ret != 0:
|
||||
raise PiicoError("close session failed", ret)
|
||||
129
apps/common/sdk/gm/piico/session_mixin.py
Normal file
129
apps/common/sdk/gm/piico/session_mixin.py
Normal file
@@ -0,0 +1,129 @@
|
||||
|
||||
from .ecc import *
|
||||
from .exception import PiicoError
|
||||
|
||||
|
||||
class BaseMixin:
|
||||
|
||||
def __init__(self):
|
||||
self._driver = None
|
||||
self._session = None
|
||||
|
||||
|
||||
class SM2Mixin(BaseMixin):
|
||||
def ecc_encrypt(self, public_key, plain_text, alg_id):
|
||||
|
||||
pos = 1
|
||||
k1 = bytes([0] * 32) + bytes(public_key[pos:pos + 32])
|
||||
k1 = (c_ubyte * len(k1))(*k1)
|
||||
pos += 32
|
||||
k2 = bytes([0] * 32) + bytes(public_key[pos:pos + 32])
|
||||
|
||||
pk = ECCrefPublicKey(c_uint(0x40), (c_ubyte * len(k1))(*k1), (c_ubyte * len(k2))(*k2))
|
||||
|
||||
plain_text = (c_ubyte * len(plain_text))(*plain_text)
|
||||
ecc_data = new_ecc_cipher_cla(len(plain_text))()
|
||||
ret = self._driver.SDF_ExternalEncrypt_ECC(self._session, c_int(alg_id), pointer(pk), plain_text,
|
||||
c_int(len(plain_text)), pointer(ecc_data))
|
||||
if ret != 0:
|
||||
raise Exception("ecc encrypt failed", ret)
|
||||
return ecc_data.encode()
|
||||
|
||||
def ecc_decrypt(self, private_key, cipher_text, alg_id):
|
||||
|
||||
k = bytes([0] * 32) + bytes(private_key[:32])
|
||||
vk = ECCrefPrivateKey(c_uint(0x40), (c_ubyte * len(k))(*k))
|
||||
|
||||
pos = 1
|
||||
# c1
|
||||
x = bytes([0] * 32) + bytes(cipher_text[pos:pos + 32])
|
||||
pos += 32
|
||||
y = bytes([0] * 32) + bytes(cipher_text[pos:pos + 32])
|
||||
pos += 32
|
||||
|
||||
# c2
|
||||
c = bytes(cipher_text[pos:-32])
|
||||
l = len(c)
|
||||
|
||||
# c3
|
||||
m = bytes(cipher_text[-32:])
|
||||
|
||||
ecc_data = new_ecc_cipher_cla(l)(
|
||||
(c_ubyte * 64)(*x),
|
||||
(c_ubyte * 64)(*y),
|
||||
(c_ubyte * 32)(*m),
|
||||
c_uint(l),
|
||||
(c_ubyte * l)(*c),
|
||||
)
|
||||
temp_data = (c_ubyte * l)()
|
||||
temp_data_length = c_int()
|
||||
ret = self._driver.SDF_ExternalDecrypt_ECC(self._session, c_int(alg_id), pointer(vk),
|
||||
pointer(ecc_data),
|
||||
temp_data, pointer(temp_data_length))
|
||||
if ret != 0:
|
||||
raise Exception("ecc decrypt failed", ret)
|
||||
return bytes(temp_data[:temp_data_length.value])
|
||||
|
||||
|
||||
class SM3Mixin(BaseMixin):
|
||||
def hash_init(self, alg_id):
|
||||
ret = self._driver.SDF_HashInit(self._session, c_int(alg_id), None, None, c_int(0))
|
||||
if ret != 0:
|
||||
raise PiicoError("hash init failed,alg id is {}".format(alg_id), ret)
|
||||
|
||||
def hash_update(self, data):
|
||||
data = (c_ubyte * len(data))(*data)
|
||||
ret = self._driver.SDF_HashUpdate(self._session, data, c_int(len(data)))
|
||||
if ret != 0:
|
||||
raise PiicoError("hash update failed", ret)
|
||||
|
||||
def hash_final(self):
|
||||
result_data = (c_ubyte * 32)()
|
||||
result_length = c_int()
|
||||
ret = self._driver.SDF_HashFinal(self._session, result_data, pointer(result_length))
|
||||
if ret != 0:
|
||||
raise PiicoError("hash final failed", ret)
|
||||
return bytes(result_data[:result_length.value])
|
||||
|
||||
|
||||
class SM4Mixin(BaseMixin):
|
||||
|
||||
def import_key(self, key_val):
|
||||
# to c lang
|
||||
key_val = (c_ubyte * len(key_val))(*key_val)
|
||||
|
||||
key = c_void_p()
|
||||
ret = self._driver.SDF_ImportKey(self._session, key_val, c_int(len(key_val)), pointer(key))
|
||||
if ret != 0:
|
||||
raise PiicoError("import key failed", ret)
|
||||
return key
|
||||
|
||||
def destroy_cipher_key(self, key):
|
||||
ret = self._driver.SDF_DestroyKey(self._session, key)
|
||||
if ret != 0:
|
||||
raise Exception("destroy key failed")
|
||||
|
||||
def encrypt(self, plain_text, key, alg, iv=None):
|
||||
return self.__do_cipher_action(plain_text, key, alg, iv, True)
|
||||
|
||||
def decrypt(self, cipher_text, key, alg, iv=None):
|
||||
return self.__do_cipher_action(cipher_text, key, alg, iv, False)
|
||||
|
||||
def __do_cipher_action(self, text, key, alg, iv=None, encrypt=True):
|
||||
text = (c_ubyte * len(text))(*text)
|
||||
if iv is not None:
|
||||
iv = (c_ubyte * len(iv))(*iv)
|
||||
|
||||
temp_data = (c_ubyte * len(text))()
|
||||
temp_data_length = c_int()
|
||||
if encrypt:
|
||||
ret = self._driver.SDF_Encrypt(self._session, key, c_int(alg), iv, text, c_int(len(text)), temp_data,
|
||||
pointer(temp_data_length))
|
||||
if ret != 0:
|
||||
raise PiicoError("encrypt failed", ret)
|
||||
else:
|
||||
ret = self._driver.SDF_Decrypt(self._session, key, c_int(alg), iv, text, c_int(len(text)), temp_data,
|
||||
pointer(temp_data_length))
|
||||
if ret != 0:
|
||||
raise PiicoError("decrypt failed", ret)
|
||||
return temp_data[:temp_data_length.value]
|
||||
0
apps/common/sdk/gm/piico/sign.py
Normal file
0
apps/common/sdk/gm/piico/sign.py
Normal file
@@ -17,4 +17,8 @@ class BaseSMSClient:
|
||||
def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def need_pre_check():
|
||||
return True
|
||||
|
||||
|
||||
|
||||
329
apps/common/sdk/sms/cmpp2.py
Normal file
329
apps/common/sdk/sms/cmpp2.py
Normal file
@@ -0,0 +1,329 @@
|
||||
import hashlib
|
||||
import socket
|
||||
import struct
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.exceptions import JMSException
|
||||
from .base import BaseSMSClient
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
CMPP_CONNECT = 0x00000001 # 请求连接
|
||||
CMPP_CONNECT_RESP = 0x80000001 # 请求连接应答
|
||||
CMPP_TERMINATE = 0x00000002 # 终止连接
|
||||
CMPP_TERMINATE_RESP = 0x80000002 # 终止连接应答
|
||||
CMPP_SUBMIT = 0x00000004 # 提交短信
|
||||
CMPP_SUBMIT_RESP = 0x80000004 # 提交短信应答
|
||||
CMPP_DELIVER = 0x00000005 # 短信下发
|
||||
CMPP_DELIVER_RESP = 0x80000005 # 下发短信应答
|
||||
|
||||
|
||||
class CMPPBaseRequestInstance(object):
|
||||
def __init__(self):
|
||||
self.command_id = ''
|
||||
self.body = b''
|
||||
self.length = 0
|
||||
|
||||
def get_header(self, sequence_id):
|
||||
length = struct.pack('!L', 12 + self.length)
|
||||
command_id = struct.pack('!L', self.command_id)
|
||||
sequence_id = struct.pack('!L', sequence_id)
|
||||
return length + command_id + sequence_id
|
||||
|
||||
def get_message(self, sequence_id):
|
||||
return self.get_header(sequence_id) + self.body
|
||||
|
||||
|
||||
class CMPPConnectRequestInstance(CMPPBaseRequestInstance):
|
||||
def __init__(self, sp_id, sp_secret):
|
||||
if len(sp_id) != 6:
|
||||
raise ValueError(_("sp_id is 6 bits"))
|
||||
|
||||
super().__init__()
|
||||
|
||||
source_addr = sp_id.encode('utf-8')
|
||||
sp_secret = sp_secret.encode('utf-8')
|
||||
version = struct.pack('!B', 0x02)
|
||||
timestamp = struct.pack('!L', int(self.get_now()))
|
||||
authenticator_source = source_addr + 9 * b'\x00' + sp_secret + self.get_now().encode('utf-8')
|
||||
auth_source_md5 = hashlib.md5(authenticator_source).digest()
|
||||
self.body = source_addr + auth_source_md5 + version + timestamp
|
||||
self.length = len(self.body)
|
||||
self.command_id = CMPP_CONNECT
|
||||
|
||||
@staticmethod
|
||||
def get_now():
|
||||
return time.strftime('%m%d%H%M%S', time.localtime(time.time()))
|
||||
|
||||
|
||||
class CMPPSubmitRequestInstance(CMPPBaseRequestInstance):
|
||||
def __init__(self, msg_src, dest_terminal_id, msg_content, src_id,
|
||||
service_id='', dest_usr_tl=1):
|
||||
if len(msg_content) >= 70:
|
||||
raise JMSException('The message length should be within 70 characters')
|
||||
if len(dest_terminal_id) > 100:
|
||||
raise JMSException('The number of users receiving information should be less than 100')
|
||||
|
||||
super().__init__()
|
||||
|
||||
msg_id = 8 * b'\x00'
|
||||
pk_total = struct.pack('!B', 1)
|
||||
pk_number = struct.pack('!B', 1)
|
||||
registered_delivery = struct.pack('!B', 0)
|
||||
msg_level = struct.pack('!B', 0)
|
||||
service_id = ((10 - len(service_id)) * '\x00' + service_id).encode('utf-8')
|
||||
fee_user_type = struct.pack('!B', 2)
|
||||
fee_terminal_id = ('0' * 21).encode('utf-8')
|
||||
tp_pid = struct.pack('!B', 0)
|
||||
tp_udhi = struct.pack('!B', 0)
|
||||
msg_fmt = struct.pack('!B', 8)
|
||||
fee_type = '01'.encode('utf-8')
|
||||
fee_code = '000000'.encode('utf-8')
|
||||
valid_time = ('\x00' * 17).encode('utf-8')
|
||||
at_time = ('\x00' * 17).encode('utf-8')
|
||||
src_id = ((21 - len(src_id)) * '\x00' + src_id).encode('utf-8')
|
||||
reserve = b'\x00' * 8
|
||||
_msg_length = struct.pack('!B', len(msg_content) * 2)
|
||||
_msg_src = msg_src.encode('utf-8')
|
||||
_dest_usr_tl = struct.pack('!B', dest_usr_tl)
|
||||
_msg_content = msg_content.encode('utf-16-be')
|
||||
_dest_terminal_id = b''.join([
|
||||
(i + (21 - len(i)) * '\x00').encode('utf-8') for i in dest_terminal_id
|
||||
])
|
||||
self.length = 126 + 21 * dest_usr_tl + len(_msg_content)
|
||||
self.command_id = CMPP_SUBMIT
|
||||
self.body = msg_id + pk_total + pk_number + registered_delivery \
|
||||
+ msg_level + service_id + fee_user_type + fee_terminal_id \
|
||||
+ tp_pid + tp_udhi + msg_fmt + _msg_src + fee_type + fee_code \
|
||||
+ valid_time + at_time + src_id + _dest_usr_tl + _dest_terminal_id \
|
||||
+ _msg_length + _msg_content + reserve
|
||||
|
||||
|
||||
class CMPPTerminateRequestInstance(CMPPBaseRequestInstance):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.body = b''
|
||||
self.command_id = CMPP_TERMINATE
|
||||
|
||||
|
||||
class CMPPDeliverRespRequestInstance(CMPPBaseRequestInstance):
|
||||
def __init__(self, msg_id, result=0):
|
||||
super().__init__()
|
||||
msg_id = struct.pack('!Q', msg_id)
|
||||
result = struct.pack('!B', result)
|
||||
self.length = len(self.body)
|
||||
self.body = msg_id + result
|
||||
|
||||
|
||||
class CMPPResponseInstance(object):
|
||||
def __init__(self):
|
||||
self.command_id = None
|
||||
self.length = None
|
||||
self.response_handler_map = {
|
||||
CMPP_CONNECT_RESP: self.connect_response_parse,
|
||||
CMPP_SUBMIT_RESP: self.submit_response_parse,
|
||||
CMPP_DELIVER: self.deliver_request_parse,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def connect_response_parse(body):
|
||||
status, = struct.unpack('!B', body[0:1])
|
||||
authenticator_ISMG = body[1:17]
|
||||
version, = struct.unpack('!B', body[17:18])
|
||||
return {
|
||||
'Status': status,
|
||||
'AuthenticatorISMG': authenticator_ISMG,
|
||||
'Version': version
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def submit_response_parse(body):
|
||||
msg_id = body[:8]
|
||||
result = struct.unpack('!B', body[8:9])
|
||||
return {
|
||||
'Msg_Id': msg_id, 'Result': result[0]
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def deliver_request_parse(body):
|
||||
msg_id, = struct.unpack('!Q', body[0:8])
|
||||
dest_id = body[8:29]
|
||||
service_id = body[29:39]
|
||||
tp_pid = struct.unpack('!B', body[39:40])
|
||||
tp_udhi = struct.unpack('!B', body[40:41])
|
||||
msg_fmt = struct.unpack('!B', body[41:42])
|
||||
src_terminal_id = body[42:63]
|
||||
registered_delivery = struct.unpack('!B', body[63:64])
|
||||
msg_length = struct.unpack('!B', body[64:65])
|
||||
msg_content = body[65:msg_length[0]+65]
|
||||
return {
|
||||
'Msg_Id': msg_id, 'Dest_Id': dest_id, 'Service_Id': service_id,
|
||||
'TP_pid': tp_pid, 'TP_udhi': tp_udhi, 'Msg_Fmt': msg_fmt,
|
||||
'Src_terminal_Id': src_terminal_id, 'Registered_Delivery': registered_delivery,
|
||||
'Msg_Length': msg_length, 'Msg_content': msg_content
|
||||
}
|
||||
|
||||
def parse_header(self, data):
|
||||
self.command_id, = struct.unpack('!L', data[4:8])
|
||||
sequence_id, = struct.unpack('!L', data[8:12])
|
||||
return {
|
||||
'length': self.length,
|
||||
'command_id': hex(self.command_id),
|
||||
'sequence_id': sequence_id
|
||||
}
|
||||
|
||||
def parse_body(self, body):
|
||||
response_body_func = self.response_handler_map.get(self.command_id)
|
||||
if response_body_func is None:
|
||||
raise JMSException('Unable to parse the returned result: %s' % body)
|
||||
return response_body_func(body)
|
||||
|
||||
def parse(self, data):
|
||||
self.length, = struct.unpack('!L', data[0:4])
|
||||
header = self.parse_header(data)
|
||||
body = self.parse_body(data[12:self.length])
|
||||
return header, body
|
||||
|
||||
|
||||
class CMPPClient(object):
|
||||
def __init__(self, host, port, sp_id, sp_secret, src_id, service_id):
|
||||
self.ip = host
|
||||
self.port = port
|
||||
self.sp_id = sp_id
|
||||
self.sp_secret = sp_secret
|
||||
self.src_id = src_id
|
||||
self.service_id = service_id
|
||||
self._sequence_id = 0
|
||||
self._is_connect = False
|
||||
self._times = 3
|
||||
self.__socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self._connect()
|
||||
|
||||
@property
|
||||
def sequence_id(self):
|
||||
s = self._sequence_id
|
||||
self._sequence_id += 1
|
||||
return s
|
||||
|
||||
def _connect(self):
|
||||
self.__socket.settimeout(5)
|
||||
error_msg = _('Failed to connect to the CMPP gateway server, err: {}')
|
||||
for i in range(self._times):
|
||||
try:
|
||||
self.__socket.connect((self.ip, self.port))
|
||||
except Exception as err:
|
||||
error_msg = error_msg.format(str(err))
|
||||
logger.warning(error_msg)
|
||||
time.sleep(1)
|
||||
else:
|
||||
self._is_connect = True
|
||||
break
|
||||
else:
|
||||
raise JMSException(error_msg)
|
||||
|
||||
def send(self, instance):
|
||||
if isinstance(instance, CMPPBaseRequestInstance):
|
||||
message = instance.get_message(sequence_id=self.sequence_id)
|
||||
else:
|
||||
message = instance
|
||||
self.__socket.send(message)
|
||||
|
||||
def recv(self):
|
||||
raw_length = self.__socket.recv(4)
|
||||
length, = struct.unpack('!L', raw_length)
|
||||
header, body = CMPPResponseInstance().parse(
|
||||
raw_length + self.__socket.recv(length - 4)
|
||||
)
|
||||
return header, body
|
||||
|
||||
def close(self):
|
||||
if self._is_connect:
|
||||
terminate_request = CMPPTerminateRequestInstance()
|
||||
self.send(terminate_request)
|
||||
self.__socket.close()
|
||||
|
||||
def _cmpp_connect(self):
|
||||
connect_request = CMPPConnectRequestInstance(self.sp_id, self.sp_secret)
|
||||
self.send(connect_request)
|
||||
header, body = self.recv()
|
||||
if body['Status'] != 0:
|
||||
raise JMSException('CMPPv2.0 authentication failed: %s' % body)
|
||||
|
||||
def _cmpp_send_sms(self, dest, sign_name, template_code, template_param):
|
||||
"""
|
||||
优先发送template_param中message的信息
|
||||
若该内容不存在,则根据template_code构建验证码发送
|
||||
"""
|
||||
message = template_param.get('message')
|
||||
if message is None:
|
||||
code = template_param.get('code')
|
||||
message = template_code.replace('{code}', code)
|
||||
msg = '【%s】 %s' % (sign_name, message)
|
||||
submit_request = CMPPSubmitRequestInstance(
|
||||
msg_src=self.sp_id, src_id=self.src_id, msg_content=msg,
|
||||
dest_usr_tl=len(dest), dest_terminal_id=dest,
|
||||
service_id=self.service_id
|
||||
)
|
||||
self.send(submit_request)
|
||||
header, body = self.recv()
|
||||
command_id = header.get('command_id')
|
||||
if command_id == CMPP_DELIVER:
|
||||
deliver_request = CMPPDeliverRespRequestInstance(
|
||||
msg_id=body['Msg_Id'], result=body['Result']
|
||||
)
|
||||
self.send(deliver_request)
|
||||
|
||||
def send_sms(self, dest, sign_name, template_code, template_param):
|
||||
try:
|
||||
self._cmpp_connect()
|
||||
self._cmpp_send_sms(dest, sign_name, template_code, template_param)
|
||||
except Exception as e:
|
||||
logger.error('CMPPv2.0 Error: %s', e)
|
||||
self.close()
|
||||
raise JMSException(e)
|
||||
|
||||
|
||||
class CMPP2SMS(BaseSMSClient):
|
||||
SIGN_AND_TMPL_SETTING_FIELD_PREFIX = 'CMPP2'
|
||||
|
||||
@classmethod
|
||||
def new_from_settings(cls):
|
||||
return cls(
|
||||
host=settings.CMPP2_HOST, port=settings.CMPP2_PORT,
|
||||
sp_id=settings.CMPP2_SP_ID, sp_secret=settings.CMPP2_SP_SECRET,
|
||||
service_id=settings.CMPP2_SERVICE_ID, src_id=getattr(settings, 'CMPP2_SRC_ID', ''),
|
||||
)
|
||||
|
||||
def __init__(self, host: str, port: int, sp_id: str, sp_secret: str, service_id: str, src_id=''):
|
||||
try:
|
||||
self.client = CMPPClient(
|
||||
host=host, port=port, sp_id=sp_id, sp_secret=sp_secret, src_id=src_id, service_id=service_id
|
||||
)
|
||||
except Exception as err:
|
||||
self.client = None
|
||||
logger.warning(err)
|
||||
raise JMSException(err)
|
||||
|
||||
@staticmethod
|
||||
def need_pre_check():
|
||||
return False
|
||||
|
||||
def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
|
||||
try:
|
||||
logger.info(f'CMPPv2.0 sms send: '
|
||||
f'phone_numbers={phone_numbers} '
|
||||
f'sign_name={sign_name} '
|
||||
f'template_code={template_code} '
|
||||
f'template_param={template_param}')
|
||||
self.client.send_sms(phone_numbers, sign_name, template_code, template_param)
|
||||
except Exception as e:
|
||||
raise JMSException(e)
|
||||
|
||||
|
||||
client = CMPP2SMS
|
||||
@@ -15,6 +15,8 @@ logger = get_logger(__name__)
|
||||
class BACKENDS(TextChoices):
|
||||
ALIBABA = 'alibaba', _('Alibaba cloud')
|
||||
TENCENT = 'tencent', _('Tencent cloud')
|
||||
HUAWEI = 'huawei', _('Huawei Cloud')
|
||||
CMPP2 = 'cmpp2', _('CMPP v2.0')
|
||||
|
||||
|
||||
class SMS:
|
||||
@@ -43,7 +45,7 @@ class SMS:
|
||||
sign_name = getattr(settings, f'{self.client.SIGN_AND_TMPL_SETTING_FIELD_PREFIX}_VERIFY_SIGN_NAME')
|
||||
template_code = getattr(settings, f'{self.client.SIGN_AND_TMPL_SETTING_FIELD_PREFIX}_VERIFY_TEMPLATE_CODE')
|
||||
|
||||
if not (sign_name and template_code):
|
||||
if self.client.need_pre_check() and not (sign_name and template_code):
|
||||
raise JMSException(
|
||||
code='verify_code_sign_tmpl_invalid',
|
||||
detail=_('SMS verification code signature or template invalid')
|
||||
|
||||
94
apps/common/sdk/sms/huawei.py
Normal file
94
apps/common/sdk/sms/huawei.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import requests
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from common.exceptions import JMSException
|
||||
from common.utils import get_logger
|
||||
|
||||
from .base import BaseSMSClient
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class HuaweiClient:
|
||||
def __init__(self, app_key, app_secret, url, sign_channel_num):
|
||||
self.url = url[:-1] if url.endswith('/') else url
|
||||
self.app_key = app_key
|
||||
self.app_secret = app_secret
|
||||
self.sign_channel_num = sign_channel_num
|
||||
|
||||
def build_wsse_header(self):
|
||||
now = time.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
nonce = str(uuid.uuid4()).replace('-', '')
|
||||
digest = hashlib.sha256((nonce + now + self.app_secret).encode()).hexdigest()
|
||||
digestBase64 = base64.b64encode(digest.encode()).decode()
|
||||
formatter = 'UsernameToken Username="{}",PasswordDigest="{}",Nonce="{}",Created="{}"'
|
||||
return formatter.format(self.app_key, digestBase64, nonce, now)
|
||||
|
||||
def send_sms(self, receiver, signature, template_id, template_param):
|
||||
sms_url = '%s/%s' % (self.url, 'sms/batchSendSms/v1')
|
||||
headers = {
|
||||
'Authorization': 'WSSE realm="SDP",profile="UsernameToken",type="Appkey"',
|
||||
'X-WSSE': self.build_wsse_header()
|
||||
}
|
||||
body = {
|
||||
'from': self.sign_channel_num, 'to': receiver, 'templateId': template_id,
|
||||
'templateParas': template_param, 'signature': signature
|
||||
}
|
||||
try:
|
||||
response = requests.post(sms_url, headers=headers, data=body)
|
||||
msg = response.json()
|
||||
except Exception as error:
|
||||
raise JMSException(code='response_bad', detail=error)
|
||||
return msg
|
||||
|
||||
|
||||
class HuaweiSMS(BaseSMSClient):
|
||||
SIGN_AND_TMPL_SETTING_FIELD_PREFIX = 'HUAWEI'
|
||||
|
||||
@classmethod
|
||||
def new_from_settings(cls):
|
||||
return cls(
|
||||
app_key=settings.HUAWEI_APP_KEY,
|
||||
app_secret=settings.HUAWEI_APP_SECRET,
|
||||
url=settings.HUAWEI_SMS_ENDPOINT,
|
||||
sign_channel_num=settings.HUAWEI_SIGN_CHANNEL_NUM
|
||||
)
|
||||
|
||||
def __init__(self, app_key: str, app_secret: str, url: str, sign_channel_num: str):
|
||||
self.client = HuaweiClient(app_key, app_secret, url, sign_channel_num)
|
||||
|
||||
def send_sms(
|
||||
self, phone_numbers: list, sign_name: str, template_code: str,
|
||||
template_param: OrderedDict, **kwargs
|
||||
):
|
||||
phone_numbers_str = ','.join(phone_numbers)
|
||||
template_param = '["%s"]' % template_param.get('code')
|
||||
req_params = {
|
||||
'receiver': phone_numbers_str, 'signature': sign_name,
|
||||
'template_id': template_code, 'template_param': template_param
|
||||
}
|
||||
try:
|
||||
logger.info(f'Huawei sms send: '
|
||||
f'phone_numbers={phone_numbers} '
|
||||
f'sign_name={sign_name} '
|
||||
f'template_code={template_code} '
|
||||
f'template_param={template_param}')
|
||||
|
||||
resp_msg = self.client.send_sms(**req_params)
|
||||
|
||||
except Exception as error:
|
||||
raise JMSException(code='response_bad', detail=error)
|
||||
|
||||
if resp_msg.get('code') != '000000':
|
||||
raise JMSException(code='response_bad', detail=resp_msg)
|
||||
return resp_msg
|
||||
|
||||
|
||||
client = HuaweiSMS
|
||||
@@ -1,6 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import re
|
||||
import socket
|
||||
from django.templatetags.static import static
|
||||
from collections import OrderedDict
|
||||
from itertools import chain
|
||||
import logging
|
||||
@@ -365,3 +367,25 @@ def pretty_string(data: str, max_length=128, ellipsis_str='...'):
|
||||
|
||||
def group_by_count(it, count):
|
||||
return [it[i:i+count] for i in range(0, len(it), count)]
|
||||
|
||||
|
||||
def test_ip_connectivity(host, port, timeout=0.5):
|
||||
"""
|
||||
timeout: seconds
|
||||
"""
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(timeout)
|
||||
result = sock.connect_ex((host, int(port)))
|
||||
sock.close()
|
||||
if result == 0:
|
||||
connectivity = True
|
||||
else:
|
||||
connectivity = False
|
||||
return connectivity
|
||||
|
||||
|
||||
def static_or_direct(logo_path):
|
||||
if logo_path.startswith('img/'):
|
||||
return static(logo_path)
|
||||
else:
|
||||
return logo_path
|
||||
|
||||
@@ -1,31 +1,38 @@
|
||||
import base64
|
||||
import logging
|
||||
import re
|
||||
|
||||
from Cryptodome.Cipher import AES, PKCS1_v1_5
|
||||
from Cryptodome.Util.Padding import pad
|
||||
from Cryptodome.Random import get_random_bytes
|
||||
from Cryptodome.PublicKey import RSA
|
||||
from Cryptodome.Util.Padding import pad
|
||||
from Cryptodome import Random
|
||||
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from common.sdk.gm import piico
|
||||
|
||||
def process_key(key):
|
||||
secret_pattern = re.compile(r'password|secret|key|token', re.IGNORECASE)
|
||||
|
||||
|
||||
def padding_key(key, max_length=32):
|
||||
"""
|
||||
返回32 bytes 的key
|
||||
"""
|
||||
if not isinstance(key, bytes):
|
||||
key = bytes(key, encoding='utf-8')
|
||||
|
||||
if len(key) >= 32:
|
||||
return key[:32]
|
||||
if len(key) >= max_length:
|
||||
return key[:max_length]
|
||||
|
||||
return pad(key, 32)
|
||||
while len(key) % 16 != 0:
|
||||
key += b'\0'
|
||||
return key
|
||||
|
||||
|
||||
class BaseCrypto:
|
||||
|
||||
def encrypt(self, text):
|
||||
return base64.urlsafe_b64encode(
|
||||
self._encrypt(bytes(text, encoding='utf8'))
|
||||
@@ -45,7 +52,7 @@ class BaseCrypto:
|
||||
|
||||
class GMSM4EcbCrypto(BaseCrypto):
|
||||
def __init__(self, key):
|
||||
self.key = process_key(key)
|
||||
self.key = padding_key(key, 16)
|
||||
self.sm4_encryptor = CryptSM4()
|
||||
self.sm4_encryptor.set_key(self.key, SM4_ENCRYPT)
|
||||
|
||||
@@ -59,6 +66,25 @@ class GMSM4EcbCrypto(BaseCrypto):
|
||||
return self.sm4_decryptor.crypt_ecb(data)
|
||||
|
||||
|
||||
class PiicoSM4EcbCrypto(BaseCrypto):
|
||||
|
||||
@staticmethod
|
||||
def to_16(key):
|
||||
while len(key) % 16 != 0:
|
||||
key += b'\0'
|
||||
return key # 返回bytes
|
||||
|
||||
def __init__(self, key, device: piico.Device):
|
||||
key = padding_key(key, 16)
|
||||
self.cipher = device.new_sm4_ebc_cipher(key)
|
||||
|
||||
def _encrypt(self, data: bytes) -> bytes:
|
||||
return self.cipher.encrypt(self.to_16(data))
|
||||
|
||||
def _decrypt(self, data: bytes) -> bytes:
|
||||
return self.cipher.decrypt(data)
|
||||
|
||||
|
||||
class AESCrypto:
|
||||
"""
|
||||
AES
|
||||
@@ -70,9 +96,8 @@ class AESCrypto:
|
||||
"""
|
||||
|
||||
def __init__(self, key):
|
||||
if len(key) > 32:
|
||||
key = key[:32]
|
||||
self.key = self.to_16(key)
|
||||
self.key = padding_key(key, 32)
|
||||
self.aes = AES.new(self.key, AES.MODE_ECB)
|
||||
|
||||
@staticmethod
|
||||
def to_16(key):
|
||||
@@ -87,17 +112,15 @@ class AESCrypto:
|
||||
return key # 返回bytes
|
||||
|
||||
def aes(self):
|
||||
return AES.new(self.key, AES.MODE_ECB) # 初始化加密器
|
||||
return AES.new(self.key, AES.MODE_ECB)
|
||||
|
||||
def encrypt(self, text):
|
||||
aes = self.aes()
|
||||
cipher = base64.encodebytes(aes.encrypt(self.to_16(text)))
|
||||
cipher = base64.encodebytes(self.aes.encrypt(self.to_16(text)))
|
||||
return str(cipher, encoding='utf8').replace('\n', '') # 加密
|
||||
|
||||
def decrypt(self, text):
|
||||
aes = self.aes()
|
||||
text_decoded = base64.decodebytes(bytes(text, encoding='utf8'))
|
||||
return str(aes.decrypt(text_decoded).rstrip(b'\0').decode("utf8"))
|
||||
return str(self.aes.decrypt(text_decoded).rstrip(b'\0').decode("utf8"))
|
||||
|
||||
|
||||
class AESCryptoGCM:
|
||||
@@ -106,7 +129,15 @@ class AESCryptoGCM:
|
||||
"""
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = process_key(key)
|
||||
self.key = self.process_key(key)
|
||||
|
||||
@staticmethod
|
||||
def process_key(key):
|
||||
if not isinstance(key, bytes):
|
||||
key = bytes(key, encoding='utf-8')
|
||||
if len(key) >= 32:
|
||||
return key[:32]
|
||||
return pad(key, 32)
|
||||
|
||||
def encrypt(self, text):
|
||||
"""
|
||||
@@ -133,7 +164,6 @@ class AESCryptoGCM:
|
||||
nonce = base64.b64decode(metadata[24:48])
|
||||
tag = base64.b64decode(metadata[48:])
|
||||
ciphertext = base64.b64decode(text[72:])
|
||||
|
||||
cipher = AES.new(self.key, AES.MODE_GCM, nonce=nonce)
|
||||
|
||||
cipher.update(header)
|
||||
@@ -144,11 +174,10 @@ class AESCryptoGCM:
|
||||
def get_aes_crypto(key=None, mode='GCM'):
|
||||
if key is None:
|
||||
key = settings.SECRET_KEY
|
||||
if mode == 'ECB':
|
||||
a = AESCrypto(key)
|
||||
elif mode == 'GCM':
|
||||
a = AESCryptoGCM(key)
|
||||
return a
|
||||
if mode == 'GCM':
|
||||
return AESCryptoGCM(key)
|
||||
else:
|
||||
return AESCrypto(key)
|
||||
|
||||
|
||||
def get_gm_sm4_ecb_crypto(key=None):
|
||||
@@ -156,40 +185,59 @@ def get_gm_sm4_ecb_crypto(key=None):
|
||||
return GMSM4EcbCrypto(key)
|
||||
|
||||
|
||||
def get_piico_gm_sm4_ecb_crypto(device, key=None):
|
||||
key = key or settings.SECRET_KEY
|
||||
return PiicoSM4EcbCrypto(key, device)
|
||||
|
||||
|
||||
aes_ecb_crypto = get_aes_crypto(mode='ECB')
|
||||
aes_crypto = get_aes_crypto(mode='GCM')
|
||||
gm_sm4_ecb_crypto = get_gm_sm4_ecb_crypto()
|
||||
|
||||
|
||||
class Crypto:
|
||||
cryptoes = {
|
||||
cryptor_map = {
|
||||
'aes_ecb': aes_ecb_crypto,
|
||||
'aes_gcm': aes_crypto,
|
||||
'aes': aes_crypto,
|
||||
'gm_sm4_ecb': gm_sm4_ecb_crypto,
|
||||
'gm': gm_sm4_ecb_crypto,
|
||||
}
|
||||
cryptos = []
|
||||
|
||||
def __init__(self):
|
||||
cryptoes = self.__class__.cryptoes.copy()
|
||||
crypto = cryptoes.pop(settings.SECURITY_DATA_CRYPTO_ALGO, None)
|
||||
if crypto is None:
|
||||
crypt_algo = settings.SECURITY_DATA_CRYPTO_ALGO
|
||||
if not crypt_algo:
|
||||
if settings.GMSSL_ENABLED:
|
||||
if settings.PIICO_DEVICE_ENABLE:
|
||||
piico_driver_path = settings.PIICO_DRIVER_PATH if settings.PIICO_DRIVER_PATH \
|
||||
else "./lib/libpiico_ccmu.so"
|
||||
device = piico.open_piico_device(piico_driver_path)
|
||||
self.cryptor_map["piico_gm"] = get_piico_gm_sm4_ecb_crypto(device)
|
||||
crypt_algo = 'piico_gm'
|
||||
else:
|
||||
crypt_algo = 'gm'
|
||||
else:
|
||||
crypt_algo = 'aes'
|
||||
cryptor = self.cryptor_map.get(crypt_algo, None)
|
||||
if cryptor is None:
|
||||
raise ImproperlyConfigured(
|
||||
f'Crypto method not supported {settings.SECURITY_DATA_CRYPTO_ALGO}'
|
||||
)
|
||||
self.cryptoes = [crypto, *cryptoes.values()]
|
||||
others = set(self.cryptor_map.values()) - {cryptor}
|
||||
self.cryptos = [cryptor, *others]
|
||||
|
||||
@property
|
||||
def encryptor(self):
|
||||
return self.cryptoes[0]
|
||||
return self.cryptos[0]
|
||||
|
||||
def encrypt(self, text):
|
||||
return self.encryptor.encrypt(text)
|
||||
|
||||
def decrypt(self, text):
|
||||
for decryptor in self.cryptoes:
|
||||
for cryptor in self.cryptos:
|
||||
try:
|
||||
origin_text = decryptor.decrypt(text)
|
||||
origin_text = cryptor.decrypt(text)
|
||||
if origin_text:
|
||||
# 有时不同算法解密不报错,但是返回空字符串
|
||||
return origin_text
|
||||
@@ -255,6 +303,8 @@ def decrypt_password(value):
|
||||
if len(cipher) != 2:
|
||||
return value
|
||||
key_cipher, password_cipher = cipher
|
||||
if not all([key_cipher, password_cipher]):
|
||||
return value
|
||||
aes_key = rsa_decrypt_by_session_pkey(key_cipher)
|
||||
aes = get_aes_crypto(aes_key, 'ECB')
|
||||
try:
|
||||
|
||||
@@ -196,7 +196,8 @@ def encrypt_password(password, salt=None, algorithm='sha512'):
|
||||
return des_crypt.hash(password, salt=salt[:2])
|
||||
|
||||
support_algorithm = {
|
||||
'sha512': sha512, 'des': des
|
||||
'sha512': sha512,
|
||||
'des': des
|
||||
}
|
||||
|
||||
if isinstance(algorithm, str):
|
||||
@@ -222,9 +223,6 @@ def ensure_last_char_is_ascii(data):
|
||||
remain = ''
|
||||
|
||||
|
||||
secret_pattern = re.compile(r'password|secret|key', re.IGNORECASE)
|
||||
|
||||
|
||||
def data_to_json(data, sort_keys=True, indent=2, cls=None):
|
||||
if cls is None:
|
||||
cls = DjangoJSONEncoder
|
||||
|
||||
@@ -4,6 +4,10 @@ import csv
|
||||
import pyzipper
|
||||
import requests
|
||||
|
||||
from hashlib import md5
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def create_csv_file(filename, headers, rows, ):
|
||||
with open(filename, 'w', encoding='utf-8-sig')as f:
|
||||
@@ -28,3 +32,18 @@ def download_file(src, path):
|
||||
with open(path, 'wb') as f:
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
|
||||
|
||||
def save_content_to_temp_path(content, file_mode=0o400):
|
||||
if not content:
|
||||
return
|
||||
|
||||
project_dir = settings.PROJECT_DIR
|
||||
tmp_dir = os.path.join(project_dir, 'tmp')
|
||||
filename = '.' + md5(content.encode('utf-8')).hexdigest()
|
||||
filepath = os.path.join(tmp_dir, filename)
|
||||
if not os.path.exists(filepath):
|
||||
with open(filepath, 'w') as f:
|
||||
f.write(content)
|
||||
os.chmod(filepath, file_mode)
|
||||
return filepath
|
||||
|
||||
@@ -15,18 +15,23 @@ import errno
|
||||
import json
|
||||
import yaml
|
||||
import copy
|
||||
import base64
|
||||
import logging
|
||||
from importlib import import_module
|
||||
from urllib.parse import urljoin, urlparse
|
||||
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
|
||||
|
||||
from django.urls import reverse_lazy
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
PROJECT_DIR = os.path.dirname(BASE_DIR)
|
||||
XPACK_DIR = os.path.join(BASE_DIR, 'xpack')
|
||||
HAS_XPACK = os.path.isdir(XPACK_DIR)
|
||||
|
||||
logger = logging.getLogger('jumpserver.conf')
|
||||
|
||||
|
||||
def import_string(dotted_path):
|
||||
try:
|
||||
@@ -39,9 +44,9 @@ def import_string(dotted_path):
|
||||
try:
|
||||
return getattr(module, class_name)
|
||||
except AttributeError as err:
|
||||
raise ImportError('Module "%s" does not define a "%s" attribute/class' % (
|
||||
module_path, class_name)
|
||||
) from err
|
||||
raise ImportError(
|
||||
'Module "%s" does not define a "%s" attribute/class' %
|
||||
(module_path, class_name)) from err
|
||||
|
||||
|
||||
def is_absolute_uri(uri):
|
||||
@@ -80,6 +85,59 @@ class DoesNotExist(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ConfigCrypto:
|
||||
secret_keys = [
|
||||
'SECRET_KEY', 'DB_PASSWORD', 'REDIS_PASSWORD',
|
||||
]
|
||||
|
||||
def __init__(self, key):
|
||||
self.safe_key = self.process_key(key)
|
||||
self.sm4_encryptor = CryptSM4()
|
||||
self.sm4_encryptor.set_key(self.safe_key, SM4_ENCRYPT)
|
||||
|
||||
self.sm4_decryptor = CryptSM4()
|
||||
self.sm4_decryptor.set_key(self.safe_key, SM4_DECRYPT)
|
||||
|
||||
@staticmethod
|
||||
def process_key(secret_encrypt_key):
|
||||
key = secret_encrypt_key.encode()
|
||||
if len(key) >= 16:
|
||||
key = key[:16]
|
||||
else:
|
||||
key += b'\0' * (16 - len(key))
|
||||
return key
|
||||
|
||||
def encrypt(self, data):
|
||||
data = bytes(data, encoding='utf8')
|
||||
return base64.b64encode(self.sm4_encryptor.crypt_ecb(data)).decode('utf8')
|
||||
|
||||
def decrypt(self, data):
|
||||
data = base64.urlsafe_b64decode(bytes(data, encoding='utf8'))
|
||||
return self.sm4_decryptor.crypt_ecb(data).decode('utf8')
|
||||
|
||||
def decrypt_if_need(self, value, item):
|
||||
if item not in self.secret_keys:
|
||||
return value
|
||||
|
||||
try:
|
||||
plaintext = self.decrypt(value)
|
||||
if plaintext:
|
||||
value = plaintext
|
||||
except Exception as e:
|
||||
logger.error('decrypt %s error: %s', item, e)
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def get_secret_encryptor(cls):
|
||||
# 使用 SM4 加密配置文件敏感信息
|
||||
# https://the-x.cn/cryptography/Sm4.aspx
|
||||
secret_encrypt_key = os.environ.get('SECRET_ENCRYPT_KEY', '')
|
||||
if not secret_encrypt_key:
|
||||
return None
|
||||
print('Info: Using SM4 to encrypt config secret value')
|
||||
return cls(secret_encrypt_key)
|
||||
|
||||
|
||||
class Config(dict):
|
||||
"""Works exactly like a dict but provides ways to fill it from files
|
||||
or special dictionaries. There are two common patterns to populate the
|
||||
@@ -160,12 +218,15 @@ class Config(dict):
|
||||
'SESSION_COOKIE_DOMAIN': None,
|
||||
'CSRF_COOKIE_DOMAIN': None,
|
||||
'SESSION_COOKIE_NAME_PREFIX': None,
|
||||
'SESSION_COOKIE_AGE': 3600,
|
||||
'SESSION_COOKIE_AGE': 3600 * 24,
|
||||
'SESSION_EXPIRE_AT_BROWSER_CLOSE': False,
|
||||
'LOGIN_URL': reverse_lazy('authentication:login'),
|
||||
'CONNECTION_TOKEN_EXPIRATION': 5 * 60,
|
||||
|
||||
# Custom Config
|
||||
'AUTH_CUSTOM': False,
|
||||
'AUTH_CUSTOM_FILE_MD5': '',
|
||||
|
||||
# Auth LDAP settings
|
||||
'AUTH_LDAP': False,
|
||||
'AUTH_LDAP_SERVER_URI': 'ldap://localhost:389',
|
||||
@@ -265,6 +326,24 @@ class Config(dict):
|
||||
'AUTH_SAML2_PROVIDER_AUTHORIZATION_ENDPOINT': '/',
|
||||
'AUTH_SAML2_AUTHENTICATION_FAILURE_REDIRECT_URI': '/',
|
||||
|
||||
# OAuth2 认证
|
||||
'AUTH_OAUTH2': False,
|
||||
'AUTH_OAUTH2_LOGO_PATH': 'img/login_oauth2_logo.png',
|
||||
'AUTH_OAUTH2_PROVIDER': 'OAuth2',
|
||||
'AUTH_OAUTH2_ALWAYS_UPDATE_USER': True,
|
||||
'AUTH_OAUTH2_CLIENT_ID': 'client-id',
|
||||
'AUTH_OAUTH2_SCOPE': '',
|
||||
'AUTH_OAUTH2_CLIENT_SECRET': '',
|
||||
'AUTH_OAUTH2_LOGOUT_COMPLETELY': True,
|
||||
'AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT': 'https://oauth2.example.com/authorize',
|
||||
'AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT': 'https://oauth2.example.com/userinfo',
|
||||
'AUTH_OAUTH2_PROVIDER_END_SESSION_ENDPOINT': 'https://oauth2.example.com/logout',
|
||||
'AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT': 'https://oauth2.example.com/access_token',
|
||||
'AUTH_OAUTH2_ACCESS_TOKEN_METHOD': 'GET',
|
||||
'AUTH_OAUTH2_USER_ATTR_MAP': {
|
||||
'name': 'name', 'username': 'username', 'email': 'email'
|
||||
},
|
||||
|
||||
'AUTH_TEMP_TOKEN': False,
|
||||
|
||||
# 企业微信
|
||||
@@ -302,6 +381,22 @@ class Config(dict):
|
||||
'TENCENT_VERIFY_SIGN_NAME': '',
|
||||
'TENCENT_VERIFY_TEMPLATE_CODE': '',
|
||||
|
||||
'HUAWEI_APP_KEY': '',
|
||||
'HUAWEI_APP_SECRET': '',
|
||||
'HUAWEI_SMS_ENDPOINT': '',
|
||||
'HUAWEI_SIGN_CHANNEL_NUM': '',
|
||||
'HUAWEI_VERIFY_SIGN_NAME': '',
|
||||
'HUAWEI_VERIFY_TEMPLATE_CODE': '',
|
||||
|
||||
'CMPP2_HOST': '',
|
||||
'CMPP2_PORT': 7890,
|
||||
'CMPP2_SP_ID': '',
|
||||
'CMPP2_SP_SECRET': '',
|
||||
'CMPP2_SRC_ID': '',
|
||||
'CMPP2_SERVICE_ID': '',
|
||||
'CMPP2_VERIFY_SIGN_NAME': '',
|
||||
'CMPP2_VERIFY_TEMPLATE_CODE': '{code}',
|
||||
|
||||
# Email
|
||||
'EMAIL_CUSTOM_USER_CREATED_SUBJECT': _('Create account successfully'),
|
||||
'EMAIL_CUSTOM_USER_CREATED_HONORIFIC': _('Hello'),
|
||||
@@ -387,7 +482,8 @@ class Config(dict):
|
||||
'SESSION_SAVE_EVERY_REQUEST': True,
|
||||
'SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE': False,
|
||||
'SERVER_REPLAY_STORAGE': {},
|
||||
'SECURITY_DATA_CRYPTO_ALGO': 'aes',
|
||||
'SECURITY_DATA_CRYPTO_ALGO': None,
|
||||
'GMSSL_ENABLED': False,
|
||||
|
||||
# 记录清理清理
|
||||
'LOGIN_LOG_KEEP_DAYS': 200,
|
||||
@@ -405,6 +501,7 @@ class Config(dict):
|
||||
'CONNECTION_TOKEN_ENABLED': False,
|
||||
|
||||
'PERM_SINGLE_ASSET_TO_UNGROUP_NODE': False,
|
||||
'TICKET_AUTHORIZE_DEFAULT_TIME': 7,
|
||||
'WINDOWS_SSH_DEFAULT_SHELL': 'cmd',
|
||||
'PERIOD_TASK_ENABLED': True,
|
||||
|
||||
@@ -416,6 +513,10 @@ class Config(dict):
|
||||
'HEALTH_CHECK_TOKEN': '',
|
||||
}
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
self.secret_encryptor = ConfigCrypto.get_secret_encryptor()
|
||||
|
||||
@staticmethod
|
||||
def convert_keycloak_to_openid(keycloak_config):
|
||||
"""
|
||||
@@ -427,7 +528,6 @@ class Config(dict):
|
||||
"""
|
||||
|
||||
openid_config = copy.deepcopy(keycloak_config)
|
||||
|
||||
auth_openid = openid_config.get('AUTH_OPENID')
|
||||
auth_openid_realm_name = openid_config.get('AUTH_OPENID_REALM_NAME')
|
||||
auth_openid_server_url = openid_config.get('AUTH_OPENID_SERVER_URL')
|
||||
@@ -556,13 +656,12 @@ class Config(dict):
|
||||
def get(self, item):
|
||||
# 再从配置文件中获取
|
||||
value = self.get_from_config(item)
|
||||
if value is not None:
|
||||
return value
|
||||
# 其次从环境变量来
|
||||
value = self.get_from_env(item)
|
||||
if value is not None:
|
||||
return value
|
||||
value = self.defaults.get(item)
|
||||
if value is None:
|
||||
value = self.get_from_env(item)
|
||||
if value is None:
|
||||
value = self.defaults.get(item)
|
||||
if self.secret_encryptor:
|
||||
value = self.secret_encryptor.decrypt_if_need(value, item)
|
||||
return value
|
||||
|
||||
def __getitem__(self, item):
|
||||
|
||||
@@ -4,10 +4,10 @@ from django.core.asgi import get_asgi_application
|
||||
|
||||
from ops.urls.ws_urls import urlpatterns as ops_urlpatterns
|
||||
from notifications.urls.ws_urls import urlpatterns as notifications_urlpatterns
|
||||
from settings.urls.ws_urls import urlpatterns as setting_urlpatterns
|
||||
|
||||
urlpatterns = []
|
||||
urlpatterns += ops_urlpatterns \
|
||||
+ notifications_urlpatterns
|
||||
urlpatterns += ops_urlpatterns + notifications_urlpatterns + setting_urlpatterns
|
||||
|
||||
application = ProtocolTypeRouter({
|
||||
'websocket': AuthMiddlewareStack(
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#
|
||||
import os
|
||||
import ldap
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from ..const import CONFIG, PROJECT_DIR, BASE_DIR
|
||||
|
||||
@@ -24,9 +23,15 @@ AUTH_LDAP_GLOBAL_OPTIONS = {
|
||||
ldap.OPT_X_TLS_REQUIRE_CERT: ldap.OPT_X_TLS_NEVER,
|
||||
ldap.OPT_REFERRALS: CONFIG.AUTH_LDAP_OPTIONS_OPT_REFERRALS
|
||||
}
|
||||
LDAP_CERT_FILE = os.path.join(PROJECT_DIR, "data", "certs", "ldap_ca.pem")
|
||||
LDAP_CACERT_FILE = os.path.join(PROJECT_DIR, "data", "certs", "ldap_ca.pem")
|
||||
if os.path.isfile(LDAP_CACERT_FILE):
|
||||
AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_X_TLS_CACERTFILE] = LDAP_CACERT_FILE
|
||||
LDAP_CERT_FILE = os.path.join(PROJECT_DIR, "data", "certs", "ldap_cert.pem")
|
||||
if os.path.isfile(LDAP_CERT_FILE):
|
||||
AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_X_TLS_CACERTFILE] = LDAP_CERT_FILE
|
||||
AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_X_TLS_CERTFILE] = LDAP_CERT_FILE
|
||||
LDAP_KEY_FILE = os.path.join(PROJECT_DIR, "data", "certs", "ldap_cert.key")
|
||||
if os.path.isfile(LDAP_KEY_FILE):
|
||||
AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_X_TLS_KEYFILE] = LDAP_KEY_FILE
|
||||
# AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU
|
||||
# AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER
|
||||
# AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
|
||||
@@ -143,6 +148,24 @@ SAML2_SP_ADVANCED_SETTINGS = CONFIG.SAML2_SP_ADVANCED_SETTINGS
|
||||
SAML2_LOGIN_URL_NAME = "authentication:saml2:saml2-login"
|
||||
SAML2_LOGOUT_URL_NAME = "authentication:saml2:saml2-logout"
|
||||
|
||||
# OAuth2 auth
|
||||
AUTH_OAUTH2 = CONFIG.AUTH_OAUTH2
|
||||
AUTH_OAUTH2_LOGO_PATH = CONFIG.AUTH_OAUTH2_LOGO_PATH
|
||||
AUTH_OAUTH2_PROVIDER = CONFIG.AUTH_OAUTH2_PROVIDER
|
||||
AUTH_OAUTH2_ALWAYS_UPDATE_USER = CONFIG.AUTH_OAUTH2_ALWAYS_UPDATE_USER
|
||||
AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT = CONFIG.AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT
|
||||
AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT = CONFIG.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT
|
||||
AUTH_OAUTH2_ACCESS_TOKEN_METHOD = CONFIG.AUTH_OAUTH2_ACCESS_TOKEN_METHOD
|
||||
AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT = CONFIG.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT
|
||||
AUTH_OAUTH2_CLIENT_SECRET = CONFIG.AUTH_OAUTH2_CLIENT_SECRET
|
||||
AUTH_OAUTH2_CLIENT_ID = CONFIG.AUTH_OAUTH2_CLIENT_ID
|
||||
AUTH_OAUTH2_SCOPE = CONFIG.AUTH_OAUTH2_SCOPE
|
||||
AUTH_OAUTH2_USER_ATTR_MAP = CONFIG.AUTH_OAUTH2_USER_ATTR_MAP
|
||||
AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME = 'authentication:oauth2:login-callback'
|
||||
AUTH_OAUTH2_AUTHENTICATION_REDIRECT_URI = '/'
|
||||
AUTH_OAUTH2_AUTHENTICATION_FAILURE_REDIRECT_URI = '/'
|
||||
AUTH_OAUTH2_LOGOUT_URL_NAME = "authentication:oauth2:logout"
|
||||
|
||||
# 临时 token
|
||||
AUTH_TEMP_TOKEN = CONFIG.AUTH_TEMP_TOKEN
|
||||
|
||||
@@ -170,8 +193,9 @@ AUTH_BACKEND_DINGTALK = 'authentication.backends.sso.DingTalkAuthentication'
|
||||
AUTH_BACKEND_FEISHU = 'authentication.backends.sso.FeiShuAuthentication'
|
||||
AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.sso.AuthorizationTokenAuthentication'
|
||||
AUTH_BACKEND_SAML2 = 'authentication.backends.saml2.SAML2Backend'
|
||||
AUTH_BACKEND_OAUTH2 = 'authentication.backends.oauth2.OAuth2Backend'
|
||||
AUTH_BACKEND_TEMP_TOKEN = 'authentication.backends.token.TempTokenAuthBackend'
|
||||
|
||||
AUTH_BACKEND_CUSTOM = 'authentication.backends.custom.CustomAuthBackend'
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
# 只做权限校验
|
||||
@@ -180,12 +204,37 @@ AUTHENTICATION_BACKENDS = [
|
||||
AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY, AUTH_BACKEND_LDAP, AUTH_BACKEND_RADIUS,
|
||||
# 跳转形式
|
||||
AUTH_BACKEND_CAS, AUTH_BACKEND_OIDC_PASSWORD, AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_SAML2,
|
||||
AUTH_BACKEND_OAUTH2,
|
||||
# 扫码模式
|
||||
AUTH_BACKEND_WECOM, AUTH_BACKEND_DINGTALK, AUTH_BACKEND_FEISHU,
|
||||
# Token模式
|
||||
AUTH_BACKEND_AUTH_TOKEN, AUTH_BACKEND_SSO, AUTH_BACKEND_TEMP_TOKEN
|
||||
AUTH_BACKEND_AUTH_TOKEN, AUTH_BACKEND_SSO, AUTH_BACKEND_TEMP_TOKEN,
|
||||
]
|
||||
|
||||
|
||||
def get_file_md5(filepath):
|
||||
import hashlib
|
||||
# 创建md5对象
|
||||
m = hashlib.md5()
|
||||
with open(filepath, 'rb') as f:
|
||||
while True:
|
||||
data = f.read(4096)
|
||||
if not data:
|
||||
break
|
||||
# 更新md5对象
|
||||
m.update(data)
|
||||
# 返回md5对象
|
||||
return m.hexdigest()
|
||||
|
||||
|
||||
AUTH_CUSTOM = CONFIG.AUTH_CUSTOM
|
||||
AUTH_CUSTOM_FILE_MD5 = CONFIG.AUTH_CUSTOM_FILE_MD5
|
||||
AUTH_CUSTOM_FILE_PATH = os.path.join(PROJECT_DIR, 'data', 'auth', 'main.py')
|
||||
if AUTH_CUSTOM and AUTH_CUSTOM_FILE_MD5 == get_file_md5(AUTH_CUSTOM_FILE_PATH):
|
||||
# 自定义认证模块
|
||||
AUTHENTICATION_BACKENDS.append(AUTH_BACKEND_CUSTOM)
|
||||
|
||||
AUTHENTICATION_BACKENDS_THIRD_PARTY = [AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_CAS, AUTH_BACKEND_SAML2, AUTH_BACKEND_OAUTH2]
|
||||
ONLY_ALLOW_EXIST_USER_AUTH = CONFIG.ONLY_ALLOW_EXIST_USER_AUTH
|
||||
ONLY_ALLOW_AUTH_FROM_SOURCE = CONFIG.ONLY_ALLOW_AUTH_FROM_SOURCE
|
||||
|
||||
|
||||
@@ -43,6 +43,9 @@ DEBUG_DEV = CONFIG.DEBUG_DEV
|
||||
# Absolute url for some case, for example email link
|
||||
SITE_URL = CONFIG.SITE_URL
|
||||
|
||||
# https://docs.djangoproject.com/en/4.1/ref/settings/
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
|
||||
# LOG LEVEL
|
||||
LOG_LEVEL = CONFIG.LOG_LEVEL
|
||||
|
||||
@@ -106,6 +109,7 @@ MIDDLEWARE = [
|
||||
'authentication.backends.oidc.middleware.OIDCRefreshIDTokenMiddleware',
|
||||
'authentication.backends.cas.middleware.CASMiddleware',
|
||||
'authentication.middleware.MFAMiddleware',
|
||||
'authentication.middleware.ThirdPartyLoginMiddleware',
|
||||
'authentication.middleware.SessionCookieMiddleware',
|
||||
'simple_history.middleware.HistoryRequestMiddleware',
|
||||
]
|
||||
@@ -138,6 +142,7 @@ WSGI_APPLICATION = 'jumpserver.wsgi.application'
|
||||
|
||||
LOGIN_REDIRECT_URL = reverse_lazy('index')
|
||||
LOGIN_URL = reverse_lazy('authentication:login')
|
||||
LOGOUT_REDIRECT_URL = CONFIG.LOGOUT_REDIRECT_URL
|
||||
|
||||
SESSION_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN
|
||||
CSRF_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN
|
||||
@@ -307,6 +312,21 @@ CSRF_COOKIE_SECURE = CONFIG.CSRF_COOKIE_SECURE
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||
|
||||
PASSWORD_HASHERS = [
|
||||
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
||||
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||
]
|
||||
|
||||
|
||||
GMSSL_ENABLED = CONFIG.GMSSL_ENABLED
|
||||
GM_HASHER = 'common.hashers.PBKDF2SM3PasswordHasher'
|
||||
if GMSSL_ENABLED:
|
||||
PASSWORD_HASHERS.insert(0, GM_HASHER)
|
||||
else:
|
||||
PASSWORD_HASHERS.append(GM_HASHER)
|
||||
|
||||
# For Debug toolbar
|
||||
INTERNAL_IPS = ["127.0.0.1"]
|
||||
if os.environ.get('DEBUG_TOOLBAR', False):
|
||||
@@ -315,3 +335,4 @@ if os.environ.get('DEBUG_TOOLBAR', False):
|
||||
DEBUG_TOOLBAR_PANELS = [
|
||||
'debug_toolbar.panels.profiling.ProfilingPanel',
|
||||
]
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ TERMINAL_TELNET_REGEX = CONFIG.TERMINAL_TELNET_REGEX
|
||||
BACKEND_ASSET_USER_AUTH_VAULT = False
|
||||
|
||||
PERM_SINGLE_ASSET_TO_UNGROUP_NODE = CONFIG.PERM_SINGLE_ASSET_TO_UNGROUP_NODE
|
||||
TICKET_AUTHORIZE_DEFAULT_TIME = CONFIG.TICKET_AUTHORIZE_DEFAULT_TIME
|
||||
PERM_EXPIRED_CHECK_PERIODIC = CONFIG.PERM_EXPIRED_CHECK_PERIODIC
|
||||
WINDOWS_SSH_DEFAULT_SHELL = CONFIG.WINDOWS_SSH_DEFAULT_SHELL
|
||||
FLOWER_URL = CONFIG.FLOWER_URL
|
||||
@@ -110,6 +111,8 @@ HTTP_LISTEN_PORT = CONFIG.HTTP_LISTEN_PORT
|
||||
WS_LISTEN_PORT = CONFIG.WS_LISTEN_PORT
|
||||
LOGIN_LOG_KEEP_DAYS = CONFIG.LOGIN_LOG_KEEP_DAYS
|
||||
TASK_LOG_KEEP_DAYS = CONFIG.TASK_LOG_KEEP_DAYS
|
||||
OPERATE_LOG_KEEP_DAYS = CONFIG.OPERATE_LOG_KEEP_DAYS
|
||||
FTP_LOG_KEEP_DAYS = CONFIG.FTP_LOG_KEEP_DAYS
|
||||
ORG_CHANGE_TO_URL = CONFIG.ORG_CHANGE_TO_URL
|
||||
WINDOWS_SKIP_ALL_MANUAL_PASSWORD = CONFIG.WINDOWS_SKIP_ALL_MANUAL_PASSWORD
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0f2fdd3a7bd34a26d068fc6ce521d0ea9983c477b13536ba3f51700a554d4ae3
|
||||
size 128706
|
||||
oid sha256:4d5dcc300fa64f04b513b670af0d9717fcdb108b54ddf264971b355cc72178de
|
||||
size 132193
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9c2b13f7242beec8786179e03de895bd3e9d8d6392b74c2398409c1bfa33d9f8
|
||||
size 106088
|
||||
oid sha256:618dce0f2521591410fb16b3d0afa536333d2e4f317d75d1955fe413dc0e64cb
|
||||
size 108949
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -40,12 +40,11 @@ class OrgManager(models.Manager):
|
||||
set_current_org(org)
|
||||
return self
|
||||
|
||||
|
||||
def bulk_create(self, objs, batch_size=None, ignore_conflicts=False):
|
||||
org = get_current_org()
|
||||
for obj in objs:
|
||||
if org.is_root():
|
||||
if not self.org_id:
|
||||
if not obj.org_id:
|
||||
raise ValidationError('Please save in a organization')
|
||||
else:
|
||||
obj.org_id = org.id
|
||||
|
||||
@@ -147,6 +147,9 @@ def _clear_users_from_org(org, users):
|
||||
@receiver(post_save, sender=User)
|
||||
@on_transaction_commit
|
||||
def on_user_created_set_default_org(sender, instance, created, **kwargs):
|
||||
if not instance.id:
|
||||
# 用户已被手动删除,instance.orgs 时会使用 id 进行查找报错,所以判断不存在id时不做处理
|
||||
return
|
||||
if not created:
|
||||
return
|
||||
if instance.orgs.count() > 0:
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from applications.models import Application
|
||||
from perms.models import ApplicationPermission
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.generics import RetrieveAPIView
|
||||
|
||||
from perms import serializers
|
||||
from perms.models import ApplicationPermission
|
||||
from applications.models import Application
|
||||
from common.permissions import IsValidUser
|
||||
from ..base import BasePermissionViewSet
|
||||
|
||||
|
||||
@@ -23,7 +27,7 @@ class ApplicationPermissionViewSet(BasePermissionViewSet):
|
||||
'application_id', 'application', 'app', 'app_name'
|
||||
]
|
||||
ordering_fields = ('name',)
|
||||
ordering = ('name', )
|
||||
ordering = ('name',)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset().prefetch_related(
|
||||
@@ -53,3 +57,11 @@ class ApplicationPermissionViewSet(BasePermissionViewSet):
|
||||
queryset = self.filter_application(queryset)
|
||||
return queryset
|
||||
|
||||
|
||||
class ApplicationPermissionActionsApi(RetrieveAPIView):
|
||||
permission_classes = (IsValidUser,)
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
category = request.GET.get('category')
|
||||
actions = ApplicationPermission.get_include_actions_choices(category=category)
|
||||
return Response(data=actions)
|
||||
|
||||
@@ -9,7 +9,7 @@ class PermedAssetsWillExpireUserMsg(UserMessage):
|
||||
def __init__(self, user, assets, day_count=0):
|
||||
super().__init__(user)
|
||||
self.assets = assets
|
||||
self.day_count = day_count
|
||||
self.day_count = _('today') if day_count == 0 else day_count
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
subject = _("You permed assets is about to expire")
|
||||
@@ -41,7 +41,7 @@ class AssetPermsWillExpireForOrgAdminMsg(UserMessage):
|
||||
super().__init__(user)
|
||||
self.perms = perms
|
||||
self.org = org
|
||||
self.day_count = day_count
|
||||
self.day_count = _('today') if day_count == 0 else day_count
|
||||
|
||||
def get_items_with_url(self):
|
||||
items_with_url = []
|
||||
@@ -59,7 +59,7 @@ class AssetPermsWillExpireForOrgAdminMsg(UserMessage):
|
||||
subject = _("Asset permissions is about to expire")
|
||||
context = {
|
||||
'name': self.user.name,
|
||||
'count': self.day_count,
|
||||
'count': str(self.day_count),
|
||||
'items_with_url': items_with_url,
|
||||
'item_type': _('asset permissions of organization {}').format(self.org)
|
||||
}
|
||||
@@ -85,13 +85,13 @@ class PermedAppsWillExpireUserMsg(UserMessage):
|
||||
def __init__(self, user, apps, day_count=0):
|
||||
super().__init__(user)
|
||||
self.apps = apps
|
||||
self.day_count = day_count
|
||||
self.day_count = _('today') if day_count == 0 else day_count
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
subject = _("Your permed applications is about to expire")
|
||||
context = {
|
||||
'name': self.user.name,
|
||||
'count': self.day_count,
|
||||
'count': str(self.day_count),
|
||||
'item_type': _('permed applications'),
|
||||
'items': [str(app) for app in self.apps]
|
||||
}
|
||||
@@ -116,7 +116,7 @@ class AppPermsWillExpireForOrgAdminMsg(UserMessage):
|
||||
super().__init__(user)
|
||||
self.perms = perms
|
||||
self.org = org
|
||||
self.day_count = day_count
|
||||
self.day_count = _('today') if day_count == 0 else day_count
|
||||
|
||||
def get_items_with_url(self):
|
||||
items_with_url = []
|
||||
@@ -134,7 +134,7 @@ class AppPermsWillExpireForOrgAdminMsg(UserMessage):
|
||||
subject = _('Application permissions is about to expire')
|
||||
context = {
|
||||
'name': self.user.name,
|
||||
'count': self.day_count,
|
||||
'count': str(self.day_count),
|
||||
'item_type': _('application permissions of organization {}').format(self.org),
|
||||
'items_with_url': items
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ permission_urlpatterns = [
|
||||
|
||||
# 验证用户是否有某个应用的权限
|
||||
path('user/validate/', api.ValidateUserApplicationPermissionApi.as_view(), name='validate-user-application-permission'),
|
||||
|
||||
path('applications/actions/', api.ApplicationPermissionActionsApi.as_view(), name='application-actions'),
|
||||
]
|
||||
|
||||
application_permission_urlpatterns = [
|
||||
|
||||
@@ -5,6 +5,4 @@ from .dingtalk import *
|
||||
from .feishu import *
|
||||
from .public import *
|
||||
from .email import *
|
||||
from .alibaba_sms import *
|
||||
from .tencent_sms import *
|
||||
from .sms import *
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
from rest_framework.views import Response
|
||||
from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.exceptions import APIException
|
||||
from rest_framework import status
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.sdk.sms.alibaba import AlibabaSMS
|
||||
from settings.models import Setting
|
||||
from common.exceptions import JMSException
|
||||
|
||||
from .. import serializers
|
||||
|
||||
|
||||
class AlibabaSMSTestingAPI(GenericAPIView):
|
||||
serializer_class = serializers.AlibabaSMSSettingSerializer
|
||||
rbac_perms = {
|
||||
'POST': 'settings.change_sms'
|
||||
}
|
||||
|
||||
def post(self, request):
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
alibaba_access_key_id = serializer.validated_data['ALIBABA_ACCESS_KEY_ID']
|
||||
alibaba_access_key_secret = serializer.validated_data.get('ALIBABA_ACCESS_KEY_SECRET')
|
||||
alibaba_verify_sign_name = serializer.validated_data['ALIBABA_VERIFY_SIGN_NAME']
|
||||
alibaba_verify_template_code = serializer.validated_data['ALIBABA_VERIFY_TEMPLATE_CODE']
|
||||
test_phone = serializer.validated_data.get('SMS_TEST_PHONE')
|
||||
|
||||
if not test_phone:
|
||||
raise JMSException(code='test_phone_required', detail=_('test_phone is required'))
|
||||
|
||||
if not alibaba_access_key_secret:
|
||||
secret = Setting.objects.filter(name='ALIBABA_ACCESS_KEY_SECRET').first()
|
||||
if secret:
|
||||
alibaba_access_key_secret = secret.cleaned_value
|
||||
|
||||
alibaba_access_key_secret = alibaba_access_key_secret or ''
|
||||
|
||||
try:
|
||||
client = AlibabaSMS(
|
||||
access_key_id=alibaba_access_key_id,
|
||||
access_key_secret=alibaba_access_key_secret
|
||||
)
|
||||
|
||||
client.send_sms(
|
||||
phone_numbers=[test_phone],
|
||||
sign_name=alibaba_verify_sign_name,
|
||||
template_code=alibaba_verify_template_code,
|
||||
template_param={'code': 'test'}
|
||||
)
|
||||
return Response(status=status.HTTP_200_OK, data={'msg': _('Test success')})
|
||||
except APIException as e:
|
||||
try:
|
||||
error = e.detail['errmsg']
|
||||
except:
|
||||
error = e.detail
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': error})
|
||||
@@ -34,11 +34,14 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
|
||||
'cas': serializers.CASSettingSerializer,
|
||||
'sso': serializers.SSOSettingSerializer,
|
||||
'saml2': serializers.SAML2SettingSerializer,
|
||||
'oauth2': serializers.OAuth2SettingSerializer,
|
||||
'clean': serializers.CleaningSerializer,
|
||||
'other': serializers.OtherSettingSerializer,
|
||||
'sms': serializers.SMSSettingSerializer,
|
||||
'alibaba': serializers.AlibabaSMSSettingSerializer,
|
||||
'tencent': serializers.TencentSMSSettingSerializer,
|
||||
'huawei': serializers.HuaweiSMSSettingSerializer,
|
||||
'cmpp2': serializers.CMPP2SMSSettingSerializer,
|
||||
}
|
||||
|
||||
rbac_category_permissions = {
|
||||
@@ -113,9 +116,12 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
|
||||
return data
|
||||
|
||||
def perform_update(self, serializer):
|
||||
post_data_names = list(self.request.data.keys())
|
||||
settings_items = self.parse_serializer_data(serializer)
|
||||
serializer_data = getattr(serializer, 'data', {})
|
||||
for item in settings_items:
|
||||
if item['name'] not in post_data_names:
|
||||
continue
|
||||
changed, setting = Setting.update_or_create(**item)
|
||||
if not changed:
|
||||
continue
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
from rest_framework.generics import ListAPIView
|
||||
import importlib
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from rest_framework.generics import ListAPIView, GenericAPIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.exceptions import APIException
|
||||
from rest_framework import status
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.sdk.sms import BACKENDS
|
||||
from common.exceptions import JMSException
|
||||
from settings.serializers.sms import SMSBackendSerializer
|
||||
from settings.models import Setting
|
||||
|
||||
from .. import serializers
|
||||
|
||||
|
||||
class SMSBackendAPI(ListAPIView):
|
||||
@@ -21,3 +32,128 @@ class SMSBackendAPI(ListAPIView):
|
||||
]
|
||||
|
||||
return Response(data)
|
||||
|
||||
|
||||
class SMSTestingAPI(GenericAPIView):
|
||||
backends_serializer = {
|
||||
'alibaba': serializers.AlibabaSMSSettingSerializer,
|
||||
'tencent': serializers.TencentSMSSettingSerializer,
|
||||
'huawei': serializers.HuaweiSMSSettingSerializer,
|
||||
'cmpp2': serializers.CMPP2SMSSettingSerializer
|
||||
}
|
||||
rbac_perms = {
|
||||
'POST': 'settings.change_sms'
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_or_from_setting(key, value=''):
|
||||
if not value:
|
||||
secret = Setting.objects.filter(name=key).first()
|
||||
if secret:
|
||||
value = secret.cleaned_value
|
||||
|
||||
return value or ''
|
||||
|
||||
def get_alibaba_params(self, data):
|
||||
init_params = {
|
||||
'access_key_id': data['ALIBABA_ACCESS_KEY_ID'],
|
||||
'access_key_secret': self.get_or_from_setting(
|
||||
'ALIBABA_ACCESS_KEY_SECRET', data.get('ALIBABA_ACCESS_KEY_SECRET')
|
||||
)
|
||||
}
|
||||
send_sms_params = {
|
||||
'sign_name': data['ALIBABA_VERIFY_SIGN_NAME'],
|
||||
'template_code': data['ALIBABA_VERIFY_TEMPLATE_CODE'],
|
||||
'template_param': {'code': '666666'}
|
||||
}
|
||||
return init_params, send_sms_params
|
||||
|
||||
def get_tencent_params(self, data):
|
||||
init_params = {
|
||||
'secret_id': data['TENCENT_SECRET_ID'],
|
||||
'secret_key': self.get_or_from_setting(
|
||||
'TENCENT_SECRET_KEY', data.get('TENCENT_SECRET_KEY')
|
||||
),
|
||||
'sdkappid': data['TENCENT_SDKAPPID']
|
||||
}
|
||||
send_sms_params = {
|
||||
'sign_name': data['TENCENT_VERIFY_SIGN_NAME'],
|
||||
'template_code': data['TENCENT_VERIFY_TEMPLATE_CODE'],
|
||||
'template_param': OrderedDict(code='666666')
|
||||
}
|
||||
return init_params, send_sms_params
|
||||
|
||||
def get_huawei_params(self, data):
|
||||
init_params = {
|
||||
'app_key': data['HUAWEI_APP_KEY'],
|
||||
'app_secret': self.get_or_from_setting(
|
||||
'HUAWEI_APP_SECRET', data.get('HUAWEI_APP_SECRET')
|
||||
),
|
||||
'url': data['HUAWEI_SMS_ENDPOINT'],
|
||||
'sign_channel_num': data['HUAWEI_SIGN_CHANNEL_NUM'],
|
||||
}
|
||||
send_sms_params = {
|
||||
'sign_name': data['HUAWEI_VERIFY_SIGN_NAME'],
|
||||
'template_code': data['HUAWEI_VERIFY_TEMPLATE_CODE'],
|
||||
'template_param': OrderedDict(code='666666')
|
||||
}
|
||||
return init_params, send_sms_params
|
||||
|
||||
def get_cmpp2_params(self, data):
|
||||
init_params = {
|
||||
'host': data['CMPP2_HOST'], 'port': data['CMPP2_PORT'],
|
||||
'sp_id': data['CMPP2_SP_ID'], 'src_id': data['CMPP2_SRC_ID'],
|
||||
'sp_secret': self.get_or_from_setting(
|
||||
'CMPP2_SP_SECRET', data.get('CMPP2_SP_SECRET')
|
||||
),
|
||||
'service_id': data['CMPP2_SERVICE_ID'],
|
||||
}
|
||||
send_sms_params = {
|
||||
'sign_name': data['CMPP2_VERIFY_SIGN_NAME'],
|
||||
'template_code': data['CMPP2_VERIFY_TEMPLATE_CODE'],
|
||||
'template_param': OrderedDict(code='666666')
|
||||
}
|
||||
return init_params, send_sms_params
|
||||
|
||||
def get_params_by_backend(self, backend, data):
|
||||
"""
|
||||
返回两部分参数
|
||||
1、实例化参数
|
||||
2、发送测试短信参数
|
||||
"""
|
||||
get_params_func = getattr(self, 'get_%s_params' % backend)
|
||||
return get_params_func(data)
|
||||
|
||||
def post(self, request, backend):
|
||||
serializer_class = self.backends_serializer.get(backend)
|
||||
if serializer_class is None:
|
||||
raise JMSException(_('Invalid SMS platform'))
|
||||
serializer = serializer_class(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
test_phone = serializer.validated_data.get('SMS_TEST_PHONE')
|
||||
if not test_phone:
|
||||
raise JMSException(code='test_phone_required', detail=_('test_phone is required'))
|
||||
|
||||
init_params, send_sms_params = self.get_params_by_backend(backend, serializer.validated_data)
|
||||
|
||||
m = importlib.import_module(f'common.sdk.sms.{backend}', __package__)
|
||||
try:
|
||||
client = m.client(**init_params)
|
||||
client.send_sms(
|
||||
phone_numbers=[test_phone],
|
||||
**send_sms_params
|
||||
)
|
||||
status_code = status.HTTP_200_OK
|
||||
data = {'msg': _('Test success')}
|
||||
except APIException as e:
|
||||
try:
|
||||
error = e.detail['errmsg']
|
||||
except:
|
||||
error = e.detail
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
data = {'error': error}
|
||||
except Exception as e:
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
data = {'error': str(e)}
|
||||
return Response(status=status_code, data=data)
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from rest_framework.views import Response
|
||||
from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.exceptions import APIException
|
||||
from rest_framework import status
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.sdk.sms.tencent import TencentSMS
|
||||
from settings.models import Setting
|
||||
from common.exceptions import JMSException
|
||||
|
||||
from .. import serializers
|
||||
|
||||
|
||||
class TencentSMSTestingAPI(GenericAPIView):
|
||||
serializer_class = serializers.TencentSMSSettingSerializer
|
||||
rbac_perms = {
|
||||
'POST': 'settings.change_sms'
|
||||
}
|
||||
|
||||
def post(self, request):
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
tencent_secret_id = serializer.validated_data['TENCENT_SECRET_ID']
|
||||
tencent_secret_key = serializer.validated_data.get('TENCENT_SECRET_KEY')
|
||||
tencent_verify_sign_name = serializer.validated_data['TENCENT_VERIFY_SIGN_NAME']
|
||||
tencent_verify_template_code = serializer.validated_data['TENCENT_VERIFY_TEMPLATE_CODE']
|
||||
tencent_sdkappid = serializer.validated_data.get('TENCENT_SDKAPPID')
|
||||
|
||||
test_phone = serializer.validated_data.get('SMS_TEST_PHONE')
|
||||
|
||||
if not test_phone:
|
||||
raise JMSException(code='test_phone_required', detail=_('test_phone is required'))
|
||||
|
||||
if not tencent_secret_key:
|
||||
secret = Setting.objects.filter(name='TENCENT_SECRET_KEY').first()
|
||||
if secret:
|
||||
tencent_secret_key = secret.cleaned_value
|
||||
|
||||
tencent_secret_key = tencent_secret_key or ''
|
||||
|
||||
try:
|
||||
client = TencentSMS(
|
||||
secret_id=tencent_secret_id,
|
||||
secret_key=tencent_secret_key,
|
||||
sdkappid=tencent_sdkappid
|
||||
)
|
||||
|
||||
client.send_sms(
|
||||
phone_numbers=[test_phone],
|
||||
sign_name=tencent_verify_sign_name,
|
||||
template_code=tencent_verify_template_code,
|
||||
template_param=OrderedDict(code='666666')
|
||||
)
|
||||
return Response(status=status.HTTP_200_OK, data={'msg': _('Test success')})
|
||||
except APIException as e:
|
||||
try:
|
||||
error = e.detail['errmsg']
|
||||
except:
|
||||
error = e.detail
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': error})
|
||||
@@ -1,9 +1,13 @@
|
||||
import os
|
||||
import json
|
||||
|
||||
from django.db import models
|
||||
from django.db.utils import ProgrammingError, OperationalError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.uploadedfile import InMemoryUploadedFile
|
||||
|
||||
from common.utils import signer, get_logger
|
||||
|
||||
@@ -118,6 +122,14 @@ class Setting(models.Model):
|
||||
setattr(settings, key, value)
|
||||
self.__class__.update_or_create(key, value, encrypted=False, category=self.category)
|
||||
|
||||
@classmethod
|
||||
def save_to_file(cls, value: InMemoryUploadedFile):
|
||||
filename = value.name
|
||||
filepath = f'settings/{filename}'
|
||||
path = default_storage.save(filepath, ContentFile(value.read()))
|
||||
url = default_storage.url(path)
|
||||
return url
|
||||
|
||||
@classmethod
|
||||
def update_or_create(cls, name='', value='', encrypted=False, category=''):
|
||||
"""
|
||||
@@ -128,6 +140,10 @@ class Setting(models.Model):
|
||||
changed = False
|
||||
if not setting:
|
||||
setting = Setting(name=name, encrypted=encrypted, category=category)
|
||||
|
||||
if isinstance(value, InMemoryUploadedFile):
|
||||
value = cls.save_to_file(value)
|
||||
|
||||
if setting.cleaned_value != value:
|
||||
setting.encrypted = encrypted
|
||||
setting.cleaned_value = value
|
||||
|
||||
@@ -9,3 +9,4 @@ from .sso import *
|
||||
from .base import *
|
||||
from .sms import *
|
||||
from .saml2 import *
|
||||
from .oauth2 import *
|
||||
|
||||
59
apps/settings/serializers/auth/oauth2.py
Normal file
59
apps/settings/serializers/auth/oauth2.py
Normal file
@@ -0,0 +1,59 @@
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
from common.utils import static_or_direct
|
||||
|
||||
__all__ = [
|
||||
'OAuth2SettingSerializer',
|
||||
]
|
||||
|
||||
|
||||
class SettingImageField(serializers.ImageField):
|
||||
def to_representation(self, value):
|
||||
return static_or_direct(value)
|
||||
|
||||
|
||||
class OAuth2SettingSerializer(serializers.Serializer):
|
||||
AUTH_OAUTH2 = serializers.BooleanField(
|
||||
default=False, label=_('Enable OAuth2 Auth')
|
||||
)
|
||||
AUTH_OAUTH2_LOGO_PATH = SettingImageField(
|
||||
allow_null=True, required=False, label=_('Logo')
|
||||
)
|
||||
AUTH_OAUTH2_PROVIDER = serializers.CharField(
|
||||
required=True, max_length=16, label=_('Service provider')
|
||||
)
|
||||
AUTH_OAUTH2_CLIENT_ID = serializers.CharField(
|
||||
required=True, max_length=1024, label=_('Client Id')
|
||||
)
|
||||
AUTH_OAUTH2_CLIENT_SECRET = EncryptedField(
|
||||
required=False, max_length=1024, label=_('Client Secret')
|
||||
)
|
||||
AUTH_OAUTH2_SCOPE = serializers.CharField(
|
||||
required=True, max_length=1024, label=_('Scope'), allow_blank=True
|
||||
)
|
||||
AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT = serializers.CharField(
|
||||
required=True, max_length=1024, label=_('Provider auth endpoint')
|
||||
)
|
||||
AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT = serializers.CharField(
|
||||
required=True, max_length=1024, label=_('Provider token endpoint')
|
||||
)
|
||||
AUTH_OAUTH2_ACCESS_TOKEN_METHOD = serializers.ChoiceField(
|
||||
default='GET', label=_('Client authentication method'),
|
||||
choices=(('GET', 'GET'), ('POST', 'POST'))
|
||||
)
|
||||
AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT = serializers.CharField(
|
||||
required=True, max_length=1024, label=_('Provider userinfo endpoint')
|
||||
)
|
||||
AUTH_OAUTH2_PROVIDER_END_SESSION_ENDPOINT = serializers.CharField(
|
||||
required=False, max_length=1024, label=_('Provider end session endpoint')
|
||||
)
|
||||
AUTH_OAUTH2_LOGOUT_COMPLETELY = serializers.BooleanField(required=False, label=_('Logout completely'))
|
||||
AUTH_OAUTH2_USER_ATTR_MAP = serializers.DictField(
|
||||
required=True, label=_('User attr map')
|
||||
)
|
||||
AUTH_OAUTH2_ALWAYS_UPDATE_USER = serializers.BooleanField(
|
||||
default=True, label=_('Always update user')
|
||||
)
|
||||
@@ -2,15 +2,19 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
from common.validators import PhoneValidator
|
||||
from common.sdk.sms import BACKENDS
|
||||
|
||||
__all__ = ['SMSSettingSerializer', 'AlibabaSMSSettingSerializer', 'TencentSMSSettingSerializer']
|
||||
__all__ = [
|
||||
'SMSSettingSerializer', 'AlibabaSMSSettingSerializer', 'TencentSMSSettingSerializer',
|
||||
'HuaweiSMSSettingSerializer', 'CMPP2SMSSettingSerializer'
|
||||
]
|
||||
|
||||
|
||||
class SMSSettingSerializer(serializers.Serializer):
|
||||
SMS_ENABLED = serializers.BooleanField(default=False, label=_('Enable SMS'))
|
||||
SMS_BACKEND = serializers.ChoiceField(
|
||||
choices=BACKENDS.choices, default=BACKENDS.ALIBABA, label=_('SMS provider')
|
||||
choices=BACKENDS.choices, default=BACKENDS.ALIBABA, label=_('SMS provider / Protocol')
|
||||
)
|
||||
|
||||
|
||||
@@ -20,7 +24,10 @@ class SignTmplPairSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class BaseSMSSettingSerializer(serializers.Serializer):
|
||||
SMS_TEST_PHONE = serializers.CharField(max_length=256, required=False, allow_blank=True, label=_('Test phone'))
|
||||
SMS_TEST_PHONE = serializers.CharField(
|
||||
max_length=256, required=False, validators=[PhoneValidator(), ],
|
||||
allow_blank=True, label=_('Test phone')
|
||||
)
|
||||
|
||||
def to_representation(self, instance):
|
||||
data = super().to_representation(instance)
|
||||
@@ -43,3 +50,38 @@ class TencentSMSSettingSerializer(BaseSMSSettingSerializer):
|
||||
TENCENT_SDKAPPID = serializers.CharField(max_length=256, required=True, label='SDK app id')
|
||||
TENCENT_VERIFY_SIGN_NAME = serializers.CharField(max_length=256, required=True, label=_('Signature'))
|
||||
TENCENT_VERIFY_TEMPLATE_CODE = serializers.CharField(max_length=256, required=True, label=_('Template code'))
|
||||
|
||||
|
||||
class HuaweiSMSSettingSerializer(BaseSMSSettingSerializer):
|
||||
HUAWEI_APP_KEY = serializers.CharField(max_length=256, required=True, label='App key')
|
||||
HUAWEI_APP_SECRET = EncryptedField(max_length=256, required=False, label='App secret')
|
||||
HUAWEI_SMS_ENDPOINT = serializers.CharField(max_length=1024, required=True, label=_('App Access Address'))
|
||||
HUAWEI_SIGN_CHANNEL_NUM = serializers.CharField(max_length=1024, required=True, label=_('Signature channel number'))
|
||||
HUAWEI_VERIFY_SIGN_NAME = serializers.CharField(max_length=256, required=True, label=_('Signature'))
|
||||
HUAWEI_VERIFY_TEMPLATE_CODE = serializers.CharField(max_length=256, required=True, label=_('Template code'))
|
||||
|
||||
|
||||
class CMPP2SMSSettingSerializer(BaseSMSSettingSerializer):
|
||||
CMPP2_HOST = serializers.CharField(max_length=256, required=True, label=_('Host'))
|
||||
CMPP2_PORT = serializers.IntegerField(default=7890, label=_('Port'))
|
||||
CMPP2_SP_ID = serializers.CharField(max_length=128, required=True, label=_('Enterprise code(SP id)'))
|
||||
CMPP2_SP_SECRET = EncryptedField(max_length=256, required=False, label=_('Shared secret(Shared secret)'))
|
||||
CMPP2_SRC_ID = serializers.CharField(max_length=256, required=False, label=_('Original number(Src id)'))
|
||||
CMPP2_SERVICE_ID = serializers.CharField(max_length=256, required=True, label=_('Business type(Service id)'))
|
||||
CMPP2_VERIFY_SIGN_NAME = serializers.CharField(max_length=256, required=True, label=_('Signature'))
|
||||
CMPP2_VERIFY_TEMPLATE_CODE = serializers.CharField(
|
||||
max_length=69, required=True, label=_('Template'),
|
||||
help_text=_('Template need contain {code} and Signature + template length does not exceed 67 words. '
|
||||
'For example, your verification code is {code}, which is valid for 5 minutes. '
|
||||
'Please do not disclose it to others.')
|
||||
)
|
||||
|
||||
def validate(self, attrs):
|
||||
sign_name = attrs.get('CMPP2_VERIFY_SIGN_NAME', '')
|
||||
template_code = attrs.get('CMPP2_VERIFY_TEMPLATE_CODE', '')
|
||||
if template_code.find('{code}') == -1:
|
||||
raise serializers.ValidationError(_('The template needs to contain {code}'))
|
||||
if len(sign_name + template_code) > 65:
|
||||
# 保证验证码内容在一条短信中(长度小于70字), 签名两边的括号和空格占3个字,再减去2个即可(验证码占用4个但占位符6个
|
||||
raise serializers.ValidationError(_('Signature + Template must not exceed 65 words'))
|
||||
return attrs
|
||||
|
||||
@@ -30,6 +30,11 @@ class OtherSettingSerializer(serializers.Serializer):
|
||||
help_text=_("Perm single to ungroup node")
|
||||
)
|
||||
|
||||
TICKET_AUTHORIZE_DEFAULT_TIME = serializers.IntegerField(
|
||||
min_value=7, max_value=9999, required=False,
|
||||
label=_("Ticket authorize default time"), help_text=_("Unit: day")
|
||||
)
|
||||
|
||||
HELP_DOCUMENT_URL = serializers.URLField(
|
||||
required=False, allow_blank=True, allow_null=True, label=_("Help Docs URL"),
|
||||
help_text=_('default: http://docs.jumpserver.org')
|
||||
|
||||
@@ -14,6 +14,7 @@ class PublicSettingSerializer(serializers.Serializer):
|
||||
class PrivateSettingSerializer(PublicSettingSerializer):
|
||||
WINDOWS_SKIP_ALL_MANUAL_PASSWORD = serializers.BooleanField()
|
||||
OLD_PASSWORD_HISTORY_LIMIT_COUNT = serializers.IntegerField()
|
||||
TICKET_AUTHORIZE_DEFAULT_TIME = serializers.IntegerField()
|
||||
SECURITY_MAX_IDLE_TIME = serializers.IntegerField()
|
||||
SECURITY_VIEW_AUTH_NEED_MFA = serializers.BooleanField()
|
||||
SECURITY_MFA_VERIFY_TTL = serializers.IntegerField()
|
||||
|
||||
@@ -7,7 +7,7 @@ from .auth import (
|
||||
LDAPSettingSerializer, OIDCSettingSerializer, KeycloakSettingSerializer,
|
||||
CASSettingSerializer, RadiusSettingSerializer, FeiShuSettingSerializer,
|
||||
WeComSettingSerializer, DingTalkSettingSerializer, AlibabaSMSSettingSerializer,
|
||||
TencentSMSSettingSerializer,
|
||||
TencentSMSSettingSerializer, CMPP2SMSSettingSerializer
|
||||
)
|
||||
from .terminal import TerminalSettingSerializer
|
||||
from .security import SecuritySettingSerializer
|
||||
@@ -37,6 +37,7 @@ class SettingsSerializer(
|
||||
CleaningSerializer,
|
||||
AlibabaSMSSettingSerializer,
|
||||
TencentSMSSettingSerializer,
|
||||
CMPP2SMSSettingSerializer,
|
||||
):
|
||||
# encrypt_fields 现在使用 write_only 来判断了
|
||||
pass
|
||||
|
||||
@@ -16,8 +16,7 @@ urlpatterns = [
|
||||
path('wecom/testing/', api.WeComTestingAPI.as_view(), name='wecom-testing'),
|
||||
path('dingtalk/testing/', api.DingTalkTestingAPI.as_view(), name='dingtalk-testing'),
|
||||
path('feishu/testing/', api.FeiShuTestingAPI.as_view(), name='feishu-testing'),
|
||||
path('alibaba/testing/', api.AlibabaSMSTestingAPI.as_view(), name='alibaba-sms-testing'),
|
||||
path('tencent/testing/', api.TencentSMSTestingAPI.as_view(), name='tencent-sms-testing'),
|
||||
path('sms/<str:backend>/testing/', api.SMSTestingAPI.as_view(), name='sms-testing'),
|
||||
path('sms/backend/', api.SMSBackendAPI.as_view(), name='sms-backend'),
|
||||
|
||||
path('setting/', api.SettingsApi.as_view(), name='settings-setting'),
|
||||
|
||||
9
apps/settings/urls/ws_urls.py
Normal file
9
apps/settings/urls/ws_urls.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.urls import path
|
||||
|
||||
from .. import ws
|
||||
|
||||
app_name = 'common'
|
||||
|
||||
urlpatterns = [
|
||||
path('ws/setting/tools/', ws.ToolsWebsocket.as_asgi(), name='setting-tools-ws'),
|
||||
]
|
||||
@@ -3,3 +3,5 @@
|
||||
|
||||
from .ldap import *
|
||||
from .common import *
|
||||
from .ping import *
|
||||
from .telnet import *
|
||||
|
||||
154
apps/settings/utils/ping.py
Normal file
154
apps/settings/utils/ping.py
Normal file
@@ -0,0 +1,154 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import os
|
||||
import select
|
||||
import socket
|
||||
import struct
|
||||
import time
|
||||
|
||||
# From /usr/include/linux/icmp.h; your milage may vary.
|
||||
ICMP_ECHO_REQUEST = 8 # Seems to be the same on Solaris.
|
||||
|
||||
|
||||
def checksum(source_string):
|
||||
"""
|
||||
I'm not too confident that this is right but testing seems
|
||||
to suggest that it gives the same answers as in_cksum in ping.c
|
||||
"""
|
||||
sum = 0
|
||||
count_to = int((len(source_string) / 2) * 2)
|
||||
for count in range(0, count_to, 2):
|
||||
this = source_string[count + 1] * 256 + source_string[count]
|
||||
sum = sum + this
|
||||
sum = sum & 0xffffffff # Necessary?
|
||||
|
||||
if count_to < len(source_string):
|
||||
sum = sum + ord(source_string[len(source_string) - 1])
|
||||
sum = sum & 0xffffffff # Necessary?
|
||||
|
||||
sum = (sum >> 16) + (sum & 0xffff)
|
||||
sum = sum + (sum >> 16)
|
||||
answer = ~sum
|
||||
answer = answer & 0xffff
|
||||
|
||||
# Swap bytes. Bugger me if I know why.
|
||||
answer = answer >> 8 | (answer << 8 & 0xff00)
|
||||
|
||||
return answer
|
||||
|
||||
|
||||
def receive_one_ping(my_socket, id, timeout):
|
||||
"""
|
||||
Receive the ping from the socket.
|
||||
"""
|
||||
time_left = timeout
|
||||
while True:
|
||||
started_select = time.time()
|
||||
what_ready = select.select([my_socket], [], [], time_left)
|
||||
how_long_in_select = time.time() - started_select
|
||||
if not what_ready[0]: # Timeout
|
||||
return
|
||||
|
||||
time_received = time.time()
|
||||
received_packet, addr = my_socket.recvfrom(1024)
|
||||
icmpHeader = received_packet[20:28]
|
||||
type, code, checksum, packet_id, sequence = struct.unpack("bbHHh", icmpHeader)
|
||||
if packet_id == id:
|
||||
bytes = struct.calcsize("d")
|
||||
time_sent = struct.unpack("d", received_packet[28: 28 + bytes])[0]
|
||||
return time_received - time_sent
|
||||
|
||||
time_left = time_left - how_long_in_select
|
||||
if time_left <= 0:
|
||||
return
|
||||
|
||||
|
||||
def send_one_ping(my_socket, dest_addr, id, psize):
|
||||
"""
|
||||
Send one ping to the given >dest_addr<.
|
||||
"""
|
||||
dest_addr = socket.gethostbyname(dest_addr)
|
||||
|
||||
# Remove header size from packet size
|
||||
# psize = psize - 8
|
||||
# laixintao edit:
|
||||
# Do not need to remove header here. From BSD ping man:
|
||||
# The default is 56, which translates into 64 ICMP data
|
||||
# bytes when combined with the 8 bytes of ICMP header data.
|
||||
|
||||
# Header is type (8), code (8), checksum (16), id (16), sequence (16)
|
||||
my_checksum = 0
|
||||
|
||||
# Make a dummy heder with a 0 checksum.
|
||||
header = struct.pack("bbHHh", ICMP_ECHO_REQUEST, 0, my_checksum, id, 1)
|
||||
bytes = struct.calcsize("d")
|
||||
data = (psize - bytes) * b"Q"
|
||||
data = struct.pack("d", time.time()) + data
|
||||
|
||||
# Calculate the checksum on the data and the dummy header.
|
||||
my_checksum = checksum(header + data)
|
||||
|
||||
# Now that we have the right checksum, we put that in. It's just easier
|
||||
# to make up a new header than to stuff it into the dummy.
|
||||
header = struct.pack(
|
||||
"bbHHh", ICMP_ECHO_REQUEST, 0, socket.htons(my_checksum), id, 1
|
||||
)
|
||||
packet = header + data
|
||||
my_socket.sendto(packet, (dest_addr, 1)) # Don't know about the 1
|
||||
|
||||
|
||||
def ping(dest_addr, timeout, psize, flag=0):
|
||||
"""
|
||||
Returns either the delay (in seconds) or none on timeout.
|
||||
"""
|
||||
icmp = socket.getprotobyname("icmp")
|
||||
try:
|
||||
if os.getuid() != 0:
|
||||
my_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, icmp)
|
||||
else:
|
||||
my_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp)
|
||||
except socket.error as e:
|
||||
if e.errno == 1:
|
||||
# Operation not permitted
|
||||
msg = str(e)
|
||||
raise socket.error(msg)
|
||||
raise # raise the original error
|
||||
|
||||
process_pre = os.getpid() & 0xFF00
|
||||
flag = flag & 0x00FF
|
||||
my_id = process_pre | flag
|
||||
|
||||
send_one_ping(my_socket, dest_addr, my_id, psize)
|
||||
delay = receive_one_ping(my_socket, my_id, timeout)
|
||||
|
||||
my_socket.close()
|
||||
return delay
|
||||
|
||||
|
||||
def verbose_ping(dest_addr, timeout=2, count=5, psize=64):
|
||||
"""
|
||||
Send `count' ping with `psize' size to `dest_addr' with
|
||||
the given `timeout' and display the result.
|
||||
"""
|
||||
for i in range(count):
|
||||
print("ping %s with ..." % dest_addr, end="")
|
||||
try:
|
||||
delay = ping(dest_addr, timeout, psize)
|
||||
except socket.gaierror as e:
|
||||
print("failed. (socket error: '%s')" % str(e))
|
||||
break
|
||||
|
||||
if delay is None:
|
||||
print("failed. (timeout within %ssec.)" % timeout)
|
||||
else:
|
||||
delay = delay * 1000
|
||||
print("get ping in %0.4fms" % delay)
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
verbose_ping("google.com")
|
||||
verbose_ping("192.168.4.1")
|
||||
verbose_ping("www.baidu.com")
|
||||
verbose_ping("sssssss")
|
||||
25
apps/settings/utils/telnet.py
Normal file
25
apps/settings/utils/telnet.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import socket
|
||||
import telnetlib
|
||||
|
||||
PROMPT_REGEX = r'[\<|\[](.*)[\>|\]]'
|
||||
|
||||
|
||||
def telnet(dest_addr, port_number=23, timeout=10):
|
||||
try:
|
||||
connection = telnetlib.Telnet(dest_addr, port_number, timeout)
|
||||
except (ConnectionRefusedError, socket.timeout, socket.gaierror) as e:
|
||||
return False, str(e)
|
||||
expected_regexes = [bytes(PROMPT_REGEX, encoding='ascii')]
|
||||
index, prompt_regex, output = connection.expect(expected_regexes, timeout=3)
|
||||
return True, output.decode('utf-8', 'ignore')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(telnet(dest_addr='1.1.1.1', port_number=2222))
|
||||
print(telnet(dest_addr='baidu.com', port_number=80))
|
||||
print(telnet(dest_addr='baidu.com', port_number=8080))
|
||||
print(telnet(dest_addr='192.168.4.1', port_number=2222))
|
||||
print(telnet(dest_addr='192.168.4.1', port_number=2223))
|
||||
print(telnet(dest_addr='ssssss', port_number=-1))
|
||||
77
apps/settings/ws.py
Normal file
77
apps/settings/ws.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import json
|
||||
|
||||
from channels.generic.websocket import JsonWebsocketConsumer
|
||||
|
||||
from common.db.utils import close_old_connections
|
||||
from common.utils import get_logger
|
||||
from .utils import ping, telnet
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ToolsWebsocket(JsonWebsocketConsumer):
|
||||
|
||||
def connect(self):
|
||||
user = self.scope["user"]
|
||||
if user.is_authenticated:
|
||||
self.accept()
|
||||
else:
|
||||
self.close()
|
||||
|
||||
def imitate_ping(self, dest_addr, timeout=3, count=5, psize=64):
|
||||
"""
|
||||
Send `count' ping with `psize' size to `dest_addr' with
|
||||
the given `timeout' and display the result.
|
||||
"""
|
||||
logger.info('receive request ping {}'.format(dest_addr))
|
||||
self.send_json({'msg': 'Trying {0}...\r\n'.format(dest_addr)})
|
||||
for i in range(count):
|
||||
msg = 'ping {0} with ...{1}\r\n'
|
||||
try:
|
||||
delay = ping(dest_addr, timeout, psize)
|
||||
except Exception as e:
|
||||
msg = msg.format(dest_addr, 'failed. (socket error: {})'.format(str(e)))
|
||||
logger.error(msg)
|
||||
self.send_json({'msg': msg})
|
||||
break
|
||||
if delay is None:
|
||||
msg = msg.format(dest_addr, 'failed. (timeout within {}sec.)'.format(timeout))
|
||||
else:
|
||||
delay = delay * 1000
|
||||
msg = msg.format(dest_addr, 'get ping in %0.4fms' % delay)
|
||||
self.send_json({'msg': msg})
|
||||
|
||||
def imitate_telnet(self, dest_addr, port_num=23, timeout=10):
|
||||
logger.info('receive request telnet {}'.format(dest_addr))
|
||||
self.send_json({'msg': 'Trying {0} {1}...\r\n'.format(dest_addr, port_num)})
|
||||
msg = 'Telnet: {}'
|
||||
try:
|
||||
is_connective, resp = telnet(dest_addr, port_num, timeout)
|
||||
if is_connective:
|
||||
msg = msg.format('Connected to {0} {1}\r\n{2}'.format(dest_addr, port_num, resp))
|
||||
else:
|
||||
msg = msg.format('Connect to {0} {1} {2}\r\nTelnet: Unable to connect to remote host'
|
||||
.format(dest_addr, port_num, resp))
|
||||
except Exception as e:
|
||||
logger.error(msg)
|
||||
msg = msg.format(str(e))
|
||||
finally:
|
||||
self.send_json({'msg': msg})
|
||||
|
||||
def receive(self, text_data=None, bytes_data=None, **kwargs):
|
||||
data = json.loads(text_data)
|
||||
tool_type = data.get('tool_type', 'Ping')
|
||||
dest_addr = data.get('dest_addr')
|
||||
if tool_type == 'Ping':
|
||||
self.imitate_ping(dest_addr)
|
||||
else:
|
||||
port_num = data.get('port_num')
|
||||
self.imitate_telnet(dest_addr, port_num)
|
||||
self.close()
|
||||
|
||||
def disconnect(self, code):
|
||||
self.close()
|
||||
close_old_connections()
|
||||
BIN
apps/static/img/login_oauth2_logo.png
Normal file
BIN
apps/static/img/login_oauth2_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
@@ -270,10 +270,12 @@ function requestApi(props) {
|
||||
if (typeof(dataBody) === "object") {
|
||||
dataBody = JSON.stringify(dataBody)
|
||||
}
|
||||
var headers = props.headers || {}
|
||||
|
||||
$.ajax({
|
||||
url: props.url,
|
||||
type: props.method || "PATCH",
|
||||
headers: headers,
|
||||
data: dataBody,
|
||||
contentType: props.content_type || "application/json; charset=utf-8",
|
||||
dataType: props.data_type || "json"
|
||||
@@ -1504,17 +1506,11 @@ function getStatusIcon(status, mapping, title) {
|
||||
|
||||
|
||||
function fillKey(key) {
|
||||
let keySize = 128
|
||||
// 如果超过 key 16 位, 最大取 32 位,需要更改填充
|
||||
if (key.length > 16) {
|
||||
key = key.slice(0, 32)
|
||||
keySize = keySize * 2
|
||||
const KeyLength = 16
|
||||
if (key.length > KeyLength) {
|
||||
key = key.slice(0, KeyLength)
|
||||
}
|
||||
const filledKeyLength = keySize / 8
|
||||
if (key.length >= filledKeyLength) {
|
||||
return key.slice(0, filledKeyLength)
|
||||
}
|
||||
const filledKey = Buffer.alloc(keySize / 8)
|
||||
const filledKey = Buffer.alloc(KeyLength)
|
||||
const keys = Buffer.from(key)
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
filledKey[i] = keys[i]
|
||||
@@ -1553,10 +1549,13 @@ function encryptPassword(password) {
|
||||
if (!password) {
|
||||
return ''
|
||||
}
|
||||
const aesKey = (Math.random() + 1).toString(36).substring(2)
|
||||
// public key 是 base64 存储的
|
||||
const rsaPublicKeyText = getCookie('jms_public_key')
|
||||
.replaceAll('"', '')
|
||||
let rsaPublicKeyText = getCookie('jms_public_key')
|
||||
if (!rsaPublicKeyText) {
|
||||
return password
|
||||
}
|
||||
const aesKey = (Math.random() + 1).toString(36).substring(2)
|
||||
rsaPublicKeyText = rsaPublicKeyText.replaceAll('"', '')
|
||||
const rsaPublicKey = atob(rsaPublicKeyText)
|
||||
const keyCipher = rsaEncrypt(aesKey, rsaPublicKey)
|
||||
const passwordCipher = aesEncrypt(password, aesKey)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user