Merge pull request #8728 from jumpserver/dev

v2.25.0-rc1
pull/8778/head
Jiangjie.Bai 2022-08-11 14:12:23 +08:00 committed by GitHub
commit 8409523fee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 2363 additions and 779 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,8 +17,10 @@ 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.NeedMoreInfoError as e:
self.send_auth_signal(success=False, reason=e.as_data().get('msg'))
return Response(e.as_data(), status=200)
def delete(self, request, *args, **kwargs):

View File

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

View File

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

View File

@ -0,0 +1,157 @@
# -*- 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 common.exceptions import JMSException
from .signals import (
oauth2_create_or_update_user, oauth2_user_login_failed,
oauth2_user_login_success
)
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'))
oauth2_user_login_success.send(sender=self.__class__, request=request, user=user)
return user
else:
logger.debug(log_prompt.format('OAuth2 user login failed'))
logger.debug(log_prompt.format('Send signal => oauth2 user login failed'))
oauth2_user_login_failed.send(
sender=self.__class__, request=request, username=user.username,
reason=_('User invalid, disabled or expired')
)
return None

View File

@ -0,0 +1,9 @@
from django.dispatch import Signal
oauth2_create_or_update_user = Signal(
providing_args=['request', 'user', 'created', 'name', 'username', 'email']
)
oauth2_user_login_success = Signal(providing_args=['request', 'user'])
oauth2_user_login_failed = Signal(providing_args=['request', 'username', 'reason'])

View File

@ -0,0 +1,11 @@
# -*- 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')
]

View File

@ -0,0 +1,58 @@
from django.views import View
from django.conf import settings
from django.contrib.auth import login
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)))
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)

View File

@ -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,10 +19,11 @@ 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
@ -127,7 +129,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)
)
}

View File

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

View File

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

View File

@ -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,7 +48,7 @@ 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)

View File

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

View File

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

View File

@ -328,13 +328,56 @@ 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
raise errors.LoginConfirmOtherError(ticket.id, ticket.get_state_display())
def get_ticket(self):
from tickets.models import ApplyLoginTicket
@ -346,44 +389,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 +487,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, '')

View File

@ -6,12 +6,16 @@ from django.core.cache import cache
from django.dispatch import receiver
from django_cas_ng.signals import cas_user_authenticated
from apps.jumpserver.settings.auth import AUTHENTICATION_BACKENDS_THIRD_PARTY
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 authentication.backends.oauth2.signals import (
oauth2_user_login_failed, oauth2_user_login_success
)
from .signals import post_auth_success, post_auth_failed
@ -25,7 +29,8 @@ 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)
@ -67,3 +72,15 @@ def on_saml2_user_login_success(sender, request, user, **kwargs):
def on_saml2_user_login_failed(sender, request, username, reason, **kwargs):
request.session['auth_backend'] = settings.AUTH_BACKEND_SAML2
post_auth_failed.send(sender, username=username, request=request, reason=reason)
@receiver(oauth2_user_login_success)
def on_oauth2_user_login_success(sender, request, user, **kwargs):
request.session['auth_backend'] = settings.AUTH_BACKEND_OAUTH2
post_auth_success.send(sender, user=user, request=request)
@receiver(oauth2_user_login_failed)
def on_oauth2_user_login_failed(sender, username, request, reason, **kwargs):
request.session['auth_backend'] = settings.AUTH_BACKEND_OAUTH2
post_auth_failed.send(sender, username=username, request=request, reason=reason)

View File

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

View File

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

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
#
from urllib.parse import urljoin
from django.conf import settings
@ -29,3 +30,23 @@ def check_different_city_login_if_need(user, request):
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 = '/'
redirect_uri = request.build_absolute_uri(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)
else:
redirect_uri = request.build_absolute_uri(path)
return redirect_uri

View File

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

View File

@ -0,0 +1 @@
from .sm3 import PBKDF2SM3PasswordHasher

View 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

View 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

View 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

View File

@ -15,6 +15,7 @@ logger = get_logger(__name__)
class BACKENDS(TextChoices):
ALIBABA = 'alibaba', _('Alibaba cloud')
TENCENT = 'tencent', _('Tencent cloud')
CMPP2 = 'cmpp2', _('CMPP v2.0')
class SMS:
@ -43,7 +44,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')

View File

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

View File

@ -1,7 +1,7 @@
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 import Random
@ -11,21 +11,25 @@ from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
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 +49,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)
@ -70,9 +74,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 +90,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 +107,7 @@ class AESCryptoGCM:
"""
def __init__(self, key):
self.key = process_key(key)
self.key = padding_key(key)
def encrypt(self, text):
"""
@ -133,7 +134,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 +144,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):
@ -162,34 +161,42 @@ 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:
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 +262,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:

View File

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

View File

@ -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,7 +218,7 @@ 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,
@ -265,6 +323,22 @@ 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_PROVIDER_AUTHORIZATION_ENDPOINT': 'https://oauth2.example.com/authorize',
'AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT': 'https://oauth2.example.com/userinfo',
'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 +376,15 @@ class Config(dict):
'TENCENT_VERIFY_SIGN_NAME': '',
'TENCENT_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 +470,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 +489,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 +501,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 +516,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 +644,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):

View File

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

View File

@ -24,9 +24,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 +149,23 @@ 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 = '/'
# 临时 token
AUTH_TEMP_TOKEN = CONFIG.AUTH_TEMP_TOKEN
@ -170,6 +193,7 @@ 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'
@ -180,12 +204,14 @@ 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
]
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

View File

@ -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',
]
@ -307,6 +311,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 +334,4 @@ if os.environ.get('DEBUG_TOOLBAR', False):
DEBUG_TOOLBAR_PANELS = [
'debug_toolbar.panels.profiling.ProfilingPanel',
]

View File

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

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0f2fdd3a7bd34a26d068fc6ce521d0ea9983c477b13536ba3f51700a554d4ae3
size 128706
oid sha256:62879a50d21ad41c43bf748f6045bf30a933d5d08021d97dc1c68e23f6bf8e65
size 130015

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9c2b13f7242beec8786179e03de895bd3e9d8d6392b74c2398409c1bfa33d9f8
size 106088
oid sha256:adb46a0b7afdddc1b8a399e0fc0a0926012308ddce0ff487b68cb6c209d74039
size 107133

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -34,11 +34,13 @@ 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,
'cmpp2': serializers.CMPP2SMSSettingSerializer,
}
rbac_category_permissions = {
@ -113,9 +115,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

View File

@ -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,111 @@ class SMSBackendAPI(ListAPIView):
]
return Response(data)
class SMSTestingAPI(GenericAPIView):
backends_serializer = {
'alibaba': serializers.AlibabaSMSSettingSerializer,
'tencent': serializers.TencentSMSSettingSerializer,
'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_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)

View File

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

View File

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

View File

@ -9,3 +9,4 @@ from .sso import *
from .base import *
from .sms import *
from .saml2 import *
from .oauth2 import *

View File

@ -0,0 +1,55 @@
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, required=False, label=_('Enable OAuth2 Auth')
)
AUTH_OAUTH2_LOGO_PATH = SettingImageField(
allow_null=True, required=False, label=_('Logo')
)
AUTH_OAUTH2_PROVIDER = serializers.CharField(
required=False, max_length=16, label=_('Service provider')
)
AUTH_OAUTH2_CLIENT_ID = serializers.CharField(
required=False, 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=False, max_length=1024, label=_('Scope'), allow_blank=True
)
AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT = serializers.CharField(
required=False, max_length=1024, label=_('Provider auth endpoint')
)
AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT = serializers.CharField(
required=False, 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=False, max_length=1024, label=_('Provider userinfo endpoint')
)
AUTH_OAUTH2_USER_ATTR_MAP = serializers.DictField(
required=False, label=_('User attr map')
)
AUTH_OAUTH2_ALWAYS_UPDATE_USER = serializers.BooleanField(
required=False, label=_('Always update user')
)

View File

@ -4,13 +4,16 @@ from rest_framework import serializers
from common.drf.fields import EncryptedField
from common.sdk.sms import BACKENDS
__all__ = ['SMSSettingSerializer', 'AlibabaSMSSettingSerializer', 'TencentSMSSettingSerializer']
__all__ = [
'SMSSettingSerializer', 'AlibabaSMSSettingSerializer', 'TencentSMSSettingSerializer',
'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')
)
@ -43,3 +46,29 @@ 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 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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -3,3 +3,5 @@
from .ldap import *
from .common import *
from .ping import *
from .telnet import *

154
apps/settings/utils/ping.py Normal file
View 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")

View 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('ascii')
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
View 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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -1504,17 +1504,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]

View File

@ -20,4 +20,4 @@ class SuperTicketStatusAPI(RetrieveDestroyAPIView):
return Ticket.objects.all()
def perform_destroy(self, instance):
instance.close(processor=instance.applicant)
instance.close()

View File

@ -1,6 +1,6 @@
from django.utils.translation import ugettext as _
from orgs.utils import tmp_to_org, tmp_to_root_org
from orgs.utils import tmp_to_org
from perms.models import ApplicationPermission
from tickets.models import ApplyApplicationTicket
from .base import BaseHandler
@ -26,6 +26,7 @@ class Handler(BaseHandler):
apply_system_users = self.ticket.apply_system_users.all()
apply_permission_name = self.ticket.apply_permission_name
apply_actions = self.ticket.apply_actions
apply_category = self.ticket.apply_category
apply_type = self.ticket.apply_type
apply_date_start = self.ticket.apply_date_start
@ -50,6 +51,7 @@ class Handler(BaseHandler):
'name': apply_permission_name,
'from_ticket': True,
'category': apply_category,
'actions': apply_actions,
'type': apply_type,
'comment': str(permission_comment),
'created_by': permission_created_by,

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.14 on 2022-07-22 08:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tickets', '0017_auto_20220623_1027'),
]
operations = [
migrations.AddField(
model_name='applyapplicationticket',
name='apply_actions',
field=models.IntegerField(choices=[(255, 'All'), (1, 'Connect'), (2, 'Upload file'), (4, 'Download file'), (6, 'Upload download'), (8, 'Clipboard copy'), (16, 'Clipboard paste'), (24, 'Clipboard copy paste')], default=255, verbose_name='Actions'),
),
]

View File

@ -1,8 +1,9 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from .general import Ticket
from perms.models import Action
from applications.const import AppCategory, AppType
from .general import Ticket
__all__ = ['ApplyApplicationTicket']
@ -22,6 +23,9 @@ class ApplyApplicationTicket(Ticket):
apply_system_users = models.ManyToManyField(
'assets.SystemUser', verbose_name=_('Apply system users'),
)
apply_actions = models.IntegerField(
choices=Action.DB_CHOICES, default=Action.ALL, verbose_name=_('Actions')
)
apply_date_start = models.DateTimeField(verbose_name=_('Date start'), null=True)
apply_date_expired = models.DateTimeField(verbose_name=_('Date expired'), null=True)
@ -32,3 +36,10 @@ class ApplyApplicationTicket(Ticket):
@property
def apply_type_display(self):
return AppType.get_label(self.apply_type)
@property
def apply_actions_display(self):
return Action.value_to_choices_display(self.apply_actions)
def get_apply_actions_display(self):
return ', '.join(self.apply_actions_display)

View File

@ -2,6 +2,7 @@ from django.utils.translation import ugettext as _
from rest_framework import serializers
from perms.models import ApplicationPermission
from perms.serializers.base import ActionsField
from orgs.utils import tmp_to_org
from applications.models import Application
from tickets.models import ApplyApplicationTicket
@ -12,6 +13,7 @@ __all__ = ['ApplyApplicationSerializer', 'ApplyApplicationDisplaySerializer', 'A
class ApplyApplicationSerializer(BaseApplyAssetApplicationSerializer, TicketApplySerializer):
apply_actions = ActionsField(required=True, allow_empty=False)
permission_model = ApplicationPermission
class Meta:
@ -19,9 +21,10 @@ class ApplyApplicationSerializer(BaseApplyAssetApplicationSerializer, TicketAppl
writeable_fields = [
'id', 'title', 'type', 'apply_category',
'apply_type', 'apply_applications', 'apply_system_users',
'apply_date_start', 'apply_date_expired', 'org_id'
'apply_actions', 'apply_date_start', 'apply_date_expired', 'org_id'
]
fields = TicketApplySerializer.Meta.fields + writeable_fields + ['apply_permission_name']
fields = TicketApplySerializer.Meta.fields + \
writeable_fields + ['apply_permission_name', 'apply_actions_display']
read_only_fields = list(set(fields) - set(writeable_fields))
ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs
extra_kwargs = {

View File

@ -23,10 +23,11 @@ class ApplyAssetSerializer(BaseApplyAssetApplicationSerializer, TicketApplySeria
model = ApplyAssetTicket
writeable_fields = [
'id', 'title', 'type', 'apply_nodes', 'apply_assets',
'apply_system_users', 'apply_actions', 'apply_actions_display',
'apply_system_users', 'apply_actions',
'apply_date_start', 'apply_date_expired', 'org_id'
]
fields = TicketApplySerializer.Meta.fields + writeable_fields + ['apply_permission_name']
fields = TicketApplySerializer.Meta.fields + \
writeable_fields + ['apply_permission_name', 'apply_actions_display']
read_only_fields = list(set(fields) - set(writeable_fields))
ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs
extra_kwargs = {

View File

@ -628,6 +628,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
radius = 'radius', 'Radius'
cas = 'cas', 'CAS'
saml2 = 'saml2', 'SAML2'
oauth2 = 'oauth2', 'OAuth2'
SOURCE_BACKEND_MAPPING = {
Source.local: [
@ -652,6 +653,9 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
Source.saml2: [
settings.AUTH_BACKEND_SAML2
],
Source.oauth2: [
settings.AUTH_BACKEND_OAUTH2
],
}
id = models.UUIDField(default=uuid.uuid4, primary_key=True)

View File

@ -9,6 +9,7 @@ from django.db.models.signals import post_save
from authentication.backends.oidc.signals import openid_create_or_update_user
from authentication.backends.saml2.signals import saml2_create_or_update_user
from authentication.backends.oauth2.signals import oauth2_create_or_update_user
from common.utils import get_logger
from common.decorator import on_transaction_commit
from .signals import post_user_create
@ -26,16 +27,18 @@ def user_authenticated_handle(user, created, source, attrs=None, **kwargs):
user.source = source
user.save()
if not created and settings.AUTH_SAML2_ALWAYS_UPDATE_USER:
if not attrs:
return
always_update = getattr(settings, 'AUTH_%s_ALWAYS_UPDATE_USER' % source.upper(), False)
if not created and always_update:
attr_whitelist = ('user', 'username', 'email', 'phone', 'comment')
logger.debug(
"Receive saml2 user updated signal: {}, "
"Receive {} user updated signal: {}, "
"Update user info: {},"
"(Update only properties in the whitelist. [{}])"
"".format(user, str(attrs), ','.join(attr_whitelist))
"".format(source, user, str(attrs), ','.join(attr_whitelist))
)
if not attrs:
return
for key, value in attrs.items():
if key in attr_whitelist and value:
setattr(user, key, value)
@ -103,6 +106,12 @@ def on_saml2_create_or_update_user(sender, user, created, attrs, **kwargs):
user_authenticated_handle(user, created, source, attrs, **kwargs)
@receiver(oauth2_create_or_update_user)
def on_oauth2_create_or_update_user(sender, user, created, attrs, **kwargs):
source = user.Source.oauth2.value
user_authenticated_handle(user, created, source, attrs, **kwargs)
@receiver(populate_user)
def on_ldap_create_user(sender, user, ldap_user, **kwargs):
if user and user.username not in ['admin']:

View File

@ -6,6 +6,7 @@
<style>
.captcha {
float: right;
width: 50%;
}
#id_captcha_1 {
width: 50%;

View File

@ -230,6 +230,8 @@ class LoginIpBlockUtil(BlockGlobalIpUtilBase):
def construct_user_email(username, email, email_suffix=''):
if email is None:
email = ''
if '@' in email:
return email
if '@' in username:

View File

@ -127,7 +127,7 @@ kubernetes==21.7.0
# DB requirements
mysqlclient==2.1.0
PyMySQL==1.0.2
cx-Oracle==8.2.1
oracledb==1.0.1
psycopg2-binary==2.9.1
pymssql==2.1.5
django-mysql==3.9.0
@ -139,3 +139,4 @@ ipython==8.4.0
ForgeryPy3==0.3.1
django-debug-toolbar==3.5
Pympler==1.0.1
IPy==1.1