Merge pull request #8846 from jumpserver/dev

v2.26.0-rc1
pull/8881/head
Jiangjie.Bai 2022-09-08 15:43:18 +08:00 committed by GitHub
commit 31de9375e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 962 additions and 150 deletions

View File

@ -17,6 +17,7 @@ ARG DEPENDENCIES=" \
libxmlsec1-dev \
libxmlsec1-openssl \
libaio-dev \
openssh-client \
sshpass"
ARG TOOLS=" \

View File

@ -16,7 +16,7 @@ from .. import const
__all__ = [
'AppSerializer', 'MiniAppSerializer', 'AppSerializerMixin',
'AppAccountSerializer', 'AppAccountSecretSerializer'
'AppAccountSerializer', 'AppAccountSecretSerializer', 'AppAccountBackUpSerializer'
]
@ -32,21 +32,23 @@ class AppSerializerMixin(serializers.Serializer):
return instance
def get_attrs_serializer(self):
default_serializer = serializers.Serializer(read_only=True)
instance = self.app
if instance:
_type = instance.type
_category = instance.category
else:
_type = self.context['request'].query_params.get('type')
_category = self.context['request'].query_params.get('category')
if _type:
if isinstance(self, AppAccountSecretSerializer):
serializer_class = type_secret_serializer_classes_mapping.get(_type)
tp = getattr(self, 'tp', None)
default_serializer = serializers.Serializer(read_only=True)
if not tp:
if instance:
tp = instance.type
category = instance.category
else:
serializer_class = type_serializer_classes_mapping.get(_type)
elif _category:
serializer_class = category_serializer_classes_mapping.get(_category)
tp = self.context['request'].query_params.get('type')
category = self.context['request'].query_params.get('category')
if tp:
if isinstance(self, AppAccountBackUpSerializer):
serializer_class = type_secret_serializer_classes_mapping.get(tp)
else:
serializer_class = type_serializer_classes_mapping.get(tp)
elif category:
serializer_class = category_serializer_classes_mapping.get(category)
else:
serializer_class = default_serializer
@ -154,11 +156,6 @@ class AppAccountSerializer(AppSerializerMixin, AuthSerializerMixin, BulkOrgResou
class AppAccountSecretSerializer(SecretReadableMixin, AppAccountSerializer):
class Meta(AppAccountSerializer.Meta):
fields_backup = [
'id', 'app_display', 'attrs', 'username', 'password', 'private_key',
'public_key', 'date_created', 'date_updated', 'version'
]
extra_kwargs = {
'password': {'write_only': False},
'private_key': {'write_only': False},
@ -166,3 +163,22 @@ class AppAccountSecretSerializer(SecretReadableMixin, AppAccountSerializer):
'app_display': {'label': _('Application display')},
'systemuser_display': {'label': _('System User')}
}
class AppAccountBackUpSerializer(AppAccountSecretSerializer):
class Meta(AppAccountSecretSerializer.Meta):
fields = [
'id', 'app_display', 'attrs', 'username', 'password', 'private_key',
'public_key', 'date_created', 'date_updated', 'version'
]
def __init__(self, *args, **kwargs):
self.tp = kwargs.pop('tp', None)
super().__init__(*args, **kwargs)
@classmethod
def setup_eager_loading(cls, queryset):
return queryset
def to_representation(self, instance):
return super(AppAccountSerializer, self).to_representation(instance)

View File

@ -13,3 +13,14 @@ class DBSerializer(serializers.Serializer):
database = serializers.CharField(
max_length=128, required=True, allow_null=True, label=_('Database')
)
use_ssl = serializers.BooleanField(default=False, label=_('Use SSL'))
ca_cert = serializers.CharField(
required=False, allow_null=True, label=_('CA certificate')
)
client_cert = serializers.CharField(
required=False, allow_null=True, label=_('Client certificate file')
)
cert_key = serializers.CharField(
required=False, allow_null=True, label=_('Certificate key file')
)
allow_invalid_cert = serializers.BooleanField(default=False, label=_('Allow invalid cert'))

View File

@ -76,10 +76,6 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
class Meta(AccountSerializer.Meta):
fields_backup = [
'hostname', 'ip', 'platform', 'protocols', 'username', 'password',
'private_key', 'public_key', 'date_created', 'date_updated', 'version'
]
extra_kwargs = {
'password': {'write_only': False},
'private_key': {'write_only': False},
@ -88,6 +84,22 @@ class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
}
class AccountBackUpSerializer(AccountSecretSerializer):
class Meta(AccountSecretSerializer.Meta):
fields = [
'id', 'hostname', 'ip', 'username', 'password',
'private_key', 'public_key', 'date_created',
'date_updated', 'version'
]
@classmethod
def setup_eager_loading(cls, queryset):
return queryset
def to_representation(self, instance):
return super(AccountSerializer, self).to_representation(instance)
class AccountTaskSerializer(serializers.Serializer):
ACTION_CHOICES = (
('test', 'test'),

View File

@ -4,15 +4,16 @@ from openpyxl import Workbook
from collections import defaultdict, OrderedDict
from django.conf import settings
from django.db.models import F
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from assets.models import AuthBook
from assets.serializers import AccountSecretSerializer
from assets.models import AuthBook, SystemUser, Asset
from assets.serializers import AccountBackUpSerializer
from assets.notifications import AccountBackupExecutionTaskMsg
from applications.models import Account
from applications.models import Account, Application
from applications.const import AppType
from applications.serializers import AppAccountSecretSerializer
from applications.serializers import AppAccountBackUpSerializer
from users.models import User
from common.utils import get_logger
from common.utils.timezone import local_now_display
@ -38,7 +39,7 @@ class BaseAccountHandler:
@classmethod
def get_header_fields(cls, serializer: serializers.Serializer):
try:
backup_fields = getattr(serializer, 'Meta').fields_backup
backup_fields = getattr(serializer, 'Meta').fields
except AttributeError:
backup_fields = serializer.fields.keys()
header_fields = {}
@ -51,17 +52,41 @@ class BaseAccountHandler:
header_fields[field] = str(v.label)
return header_fields
@staticmethod
def load_auth(tp, value, system_user):
if value:
return value
if system_user:
return getattr(system_user, tp, '')
return ''
@classmethod
def create_row(cls, account, serializer_cls, header_fields=None):
serializer = serializer_cls(account)
if not header_fields:
header_fields = cls.get_header_fields(serializer)
data = cls.unpack_data(serializer.data)
def replace_auth(cls, account, system_user_dict):
system_user = system_user_dict.get(account.systemuser_id)
account.username = cls.load_auth('username', account.username, system_user)
account.password = cls.load_auth('password', account.password, system_user)
account.private_key = cls.load_auth('private_key', account.private_key, system_user)
account.public_key = cls.load_auth('public_key', account.public_key, system_user)
return account
@classmethod
def create_row(cls, data, header_fields):
data = cls.unpack_data(data)
row_dict = {}
for field, header_name in header_fields.items():
row_dict[header_name] = str(data[field])
row_dict[header_name] = str(data.get(field, field))
return row_dict
@classmethod
def add_rows(cls, data, header_fields, sheet):
data_map = defaultdict(list)
for i in data:
row = cls.create_row(i, header_fields)
if sheet not in data_map:
data_map[sheet].append(list(row.keys()))
data_map[sheet].append(list(row.values()))
return data_map
class AssetAccountHandler(BaseAccountHandler):
@staticmethod
@ -72,22 +97,27 @@ class AssetAccountHandler(BaseAccountHandler):
return filename
@classmethod
def create_data_map(cls):
data_map = defaultdict(list)
def replace_account_info(cls, account, asset_dict, system_user_dict):
asset = asset_dict.get(account.asset_id)
account.ip = asset.ip if asset else ''
account.hostname = asset.hostname if asset else ''
account = cls.replace_auth(account, system_user_dict)
return account
@classmethod
def create_data_map(cls, system_user_dict):
sheet_name = AuthBook._meta.verbose_name
assets = Asset.objects.only('id', 'hostname', 'ip')
asset_dict = {asset.id: asset for asset in assets}
accounts = AuthBook.objects.all()
if not accounts.exists():
return
accounts = AuthBook.get_queryset().select_related('systemuser')
if not accounts.first():
return data_map
header_fields = cls.get_header_fields(AccountSecretSerializer(accounts.first()))
header_fields = cls.get_header_fields(AccountBackUpSerializer(accounts.first()))
for account in accounts:
account.load_auth()
row = cls.create_row(account, AccountSecretSerializer, header_fields)
if sheet_name not in data_map:
data_map[sheet_name].append(list(row.keys()))
data_map[sheet_name].append(list(row.values()))
cls.replace_account_info(account, asset_dict, system_user_dict)
data = AccountBackUpSerializer(accounts, many=True).data
data_map = cls.add_rows(data, header_fields, sheet_name)
logger.info('\n\033[33m- 共收集 {} 条资产账号\033[0m'.format(accounts.count()))
return data_map
@ -101,18 +131,36 @@ class AppAccountHandler(BaseAccountHandler):
return filename
@classmethod
def create_data_map(cls):
data_map = defaultdict(list)
accounts = Account.get_queryset().select_related('systemuser')
for account in accounts:
account.load_auth()
app_type = account.type
def replace_account_info(cls, account, app_dict, system_user_dict):
app = app_dict.get(account.app_id)
account.type = app.type if app else ''
account.app_display = app.name if app else ''
account.category = app.category if app else ''
account = cls.replace_auth(account, system_user_dict)
return account
@classmethod
def create_data_map(cls, system_user_dict):
apps = Application.objects.only('id', 'type', 'name', 'category')
app_dict = {app.id: app for app in apps}
qs = Account.objects.all().annotate(app_type=F('app__type'))
if not qs.exists():
return
account_type_map = defaultdict(list)
for i in qs:
account_type_map[i.app_type].append(i)
data_map = {}
for app_type, accounts in account_type_map.items():
sheet_name = AppType.get_label(app_type)
row = cls.create_row(account, AppAccountSecretSerializer)
if sheet_name not in data_map:
data_map[sheet_name].append(list(row.keys()))
data_map[sheet_name].append(list(row.values()))
logger.info('\n\033[33m- 共收集{}条应用账号\033[0m'.format(accounts.count()))
header_fields = cls.get_header_fields(AppAccountBackUpSerializer(tp=app_type))
if not accounts:
continue
for account in accounts:
cls.replace_account_info(account, app_dict, system_user_dict)
data = AppAccountBackUpSerializer(accounts, many=True, app_type=app_type).data
data_map.update(cls.add_rows(data, header_fields, sheet_name))
logger.info('\n\033[33m- 共收集{}条应用账号\033[0m'.format(qs.count()))
return data_map
@ -137,12 +185,16 @@ class AccountBackupHandler:
# Print task start date
time_start = time.time()
files = []
system_user_qs = SystemUser.objects.only(
'id', 'username', 'password', 'private_key', 'public_key'
)
system_user_dict = {i.id: i for i in system_user_qs}
for account_type in self.execution.types:
handler = handler_map.get(account_type)
if not handler:
continue
data_map = handler.create_data_map()
data_map = handler.create_data_map(system_user_dict)
if not data_map:
continue

View File

@ -0,0 +1,61 @@
from django.conf import settings
from django.utils.module_loading import import_string
from common.utils import get_logger
from django.contrib.auth import get_user_model
from authentication.signals import user_auth_failed, user_auth_success
from .base import JMSModelBackend
logger = get_logger(__file__)
custom_authenticate_method = None
if settings.AUTH_CUSTOM:
""" 保证自定义认证方法在服务运行时不能被更改,只在第一次调用时加载一次 """
try:
custom_auth_method_path = 'data.auth.main.authenticate'
custom_authenticate_method = import_string(custom_auth_method_path)
except Exception as e:
logger.warning('Import custom auth method failed: {}, Maybe not enabled'.format(e))
class CustomAuthBackend(JMSModelBackend):
def is_enabled(self):
return settings.AUTH_CUSTOM and callable(custom_authenticate_method)
@staticmethod
def get_or_create_user_from_userinfo(userinfo: dict):
username = userinfo['username']
attrs = ['name', 'username', 'email', 'is_active']
defaults = {attr: userinfo[attr] for attr in attrs}
user, created = get_user_model().objects.get_or_create(
username=username, defaults=defaults
)
return user, created
def authenticate(self, request, username=None, password=None, **kwargs):
try:
userinfo: dict = custom_authenticate_method(
username=username, password=password, **kwargs
)
user, created = self.get_or_create_user_from_userinfo(userinfo)
except Exception as e:
logger.error('Custom authenticate error: {}'.format(e))
return None
if self.user_can_authenticate(user):
logger.info(f'Custom authenticate success: {user.username}')
user_auth_success.send(
sender=self.__class__, request=request, user=user,
backend=settings.AUTH_BACKEND_CUSTOM
)
return user
else:
logger.info(f'Custom authenticate failed: {user.username}')
user_auth_failed.send(
sender=self.__class__, request=request, username=user.username,
reason=_('User invalid, disabled or expired'),
backend=settings.AUTH_BACKEND_CUSTOM
)
return None

View File

@ -10,11 +10,11 @@ from django.urls import reverse
from common.utils import get_logger
from users.utils import construct_user_email
from authentication.utils import build_absolute_uri
from authentication.signals import user_auth_failed, user_auth_success
from common.exceptions import JMSException
from .signals import (
oauth2_create_or_update_user, oauth2_user_login_failed,
oauth2_user_login_success
oauth2_create_or_update_user
)
from ..base import JMSModelBackend
@ -145,13 +145,17 @@ class OAuth2Backend(JMSModelBackend):
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)
user_auth_success.send(
sender=self.__class__, request=request, user=user,
backend=settings.AUTH_BACKEND_OAUTH2
)
return user
else:
logger.debug(log_prompt.format('OAuth2 user login failed'))
logger.debug(log_prompt.format('Send signal => oauth2 user login failed'))
oauth2_user_login_failed.send(
user_auth_failed.send(
sender=self.__class__, request=request, username=user.username,
reason=_('User invalid, disabled or expired')
reason=_('User invalid, disabled or expired'),
backend=settings.AUTH_BACKEND_OAUTH2
)
return None

View File

@ -4,6 +4,4 @@ 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

@ -7,5 +7,6 @@ from . import views
urlpatterns = [
path('login/', views.OAuth2AuthRequestView.as_view(), name='login'),
path('callback/', views.OAuth2AuthCallbackView.as_view(), name='login-callback')
path('callback/', views.OAuth2AuthCallbackView.as_view(), name='login-callback'),
path('logout/', views.OAuth2EndSessionView.as_view(), name='logout')
]

View File

@ -1,6 +1,6 @@
from django.views import View
from django.conf import settings
from django.contrib.auth import login
from django.contrib import auth
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.http import urlencode
@ -48,7 +48,7 @@ class OAuth2AuthCallbackView(View):
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)
auth.login(self.request, user)
logger.debug(log_prompt.format('Redirect'))
return HttpResponseRedirect(
settings.AUTH_OAUTH2_AUTHENTICATION_REDIRECT_URI
@ -56,3 +56,33 @@ class OAuth2AuthCallbackView(View):
logger.debug(log_prompt.format('Redirect'))
return HttpResponseRedirect(settings.AUTH_OAUTH2_AUTHENTICATION_FAILURE_REDIRECT_URI)
class OAuth2EndSessionView(View):
http_method_names = ['get', 'post', ]
def get(self, request):
""" Processes GET requests. """
log_prompt = "Process GET requests [OAuth2EndSessionView]: {}"
logger.debug(log_prompt.format('Start'))
return self.post(request)
def post(self, request):
""" Processes POST requests. """
log_prompt = "Process POST requests [OAuth2EndSessionView]: {}"
logger.debug(log_prompt.format('Start'))
logout_url = settings.LOGOUT_REDIRECT_URL or '/'
# Log out the current user.
if request.user.is_authenticated:
logger.debug(log_prompt.format('Log out the current user: {}'.format(request.user)))
auth.logout(request)
if settings.AUTH_OAUTH2_LOGOUT_COMPLETELY:
logger.debug(log_prompt.format('Log out OAUTH2 platform user session synchronously'))
next_url = settings.AUTH_OAUTH2_PROVIDER_END_SESSION_ENDPOINT
return HttpResponseRedirect(next_url)
logger.debug(log_prompt.format('Redirect'))
return HttpResponseRedirect(logout_url)

View File

@ -26,8 +26,9 @@ from ..base import JMSBaseAuthBackend
from .utils import validate_and_return_id_token
from .decorator import ssl_verification
from .signals import (
openid_create_or_update_user, openid_user_login_failed, openid_user_login_success
openid_create_or_update_user
)
from authentication.signals import user_auth_success, user_auth_failed
logger = get_logger(__file__)
@ -213,14 +214,18 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
if self.user_can_authenticate(user):
logger.debug(log_prompt.format('OpenID user login success'))
logger.debug(log_prompt.format('Send signal => openid user login success'))
openid_user_login_success.send(sender=self.__class__, request=request, user=user)
user_auth_success.send(
sender=self.__class__, request=request, user=user,
backend=settings.AUTH_BACKEND_OIDC_CODE
)
return user
else:
logger.debug(log_prompt.format('OpenID user login failed'))
logger.debug(log_prompt.format('Send signal => openid user login failed'))
openid_user_login_failed.send(
user_auth_failed.send(
sender=self.__class__, request=request, username=user.username,
reason="User is invalid"
reason="User is invalid", backend=settings.AUTH_BACKEND_OIDC_CODE
)
return None
@ -271,8 +276,9 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend):
"content is: {}, error is: {}".format(token_response.content, str(e))
logger.debug(log_prompt.format(error))
logger.debug(log_prompt.format('Send signal => openid user login failed'))
openid_user_login_failed.send(
sender=self.__class__, request=request, username=username, reason=error
user_auth_failed.send(
sender=self.__class__, request=request, username=username, reason=error,
backend=settings.AUTH_BACKEND_OIDC_PASSWORD
)
return
@ -299,8 +305,9 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend):
"content is: {}, error is: {}".format(claims_response.content, str(e))
logger.debug(log_prompt.format(error))
logger.debug(log_prompt.format('Send signal => openid user login failed'))
openid_user_login_failed.send(
sender=self.__class__, request=request, username=username, reason=error
user_auth_failed.send(
sender=self.__class__, request=request, username=username, reason=error,
backend=settings.AUTH_BACKEND_OIDC_PASSWORD
)
return
@ -312,13 +319,16 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend):
if self.user_can_authenticate(user):
logger.debug(log_prompt.format('OpenID user login success'))
logger.debug(log_prompt.format('Send signal => openid user login success'))
openid_user_login_success.send(
sender=self.__class__, request=request, user=user
user_auth_success.send(
sender=self.__class__, request=request, user=user,
backend=settings.AUTH_BACKEND_OIDC_PASSWORD
)
return user
else:
logger.debug(log_prompt.format('OpenID user login failed'))
logger.debug(log_prompt.format('Send signal => openid user login failed'))
openid_user_login_failed.send(
sender=self.__class__, request=request, username=username, reason="User is invalid"
user_auth_failed.send(
sender=self.__class__, request=request, username=username, reason="User is invalid",
backend=settings.AUTH_BACKEND_OIDC_PASSWORD
)
return None

View File

@ -13,6 +13,4 @@ from django.dispatch import Signal
openid_create_or_update_user = Signal(
providing_args=['request', 'user', 'created', 'name', 'username', 'email']
)
openid_user_login_success = Signal(providing_args=['request', 'user'])
openid_user_login_failed = Signal(providing_args=['request', 'username', 'reason'])

View File

@ -7,9 +7,9 @@ from django.db import transaction
from common.utils import get_logger
from authentication.errors import reason_choices, reason_user_invalid
from .signals import (
saml2_user_authenticated, saml2_user_authentication_failed,
saml2_create_or_update_user
)
from authentication.signals import user_auth_failed, user_auth_success
from ..base import JMSModelBackend
__all__ = ['SAML2Backend']
@ -55,14 +55,16 @@ class SAML2Backend(JMSModelBackend):
if self.user_can_authenticate(user):
logger.debug(log_prompt.format('SAML2 user login success'))
saml2_user_authenticated.send(
sender=self, request=request, user=user, created=created
user_auth_success.send(
sender=self.__class__, request=request, user=user, created=created,
backend=settings.AUTH_BACKEND_SAML2
)
return user
else:
logger.debug(log_prompt.format('SAML2 user login failed'))
saml2_user_authentication_failed.send(
sender=self, request=request, username=username,
reason=reason_choices.get(reason_user_invalid)
user_auth_failed.send(
sender=self.__class__, request=request, username=username,
reason=reason_choices.get(reason_user_invalid),
backend=settings.AUTH_BACKEND_SAML2
)
return None

View File

@ -2,5 +2,3 @@ from django.dispatch import Signal
saml2_create_or_update_user = Signal(providing_args=('user', 'created', 'request', 'attrs'))
saml2_user_authenticated = Signal(providing_args=('user', 'created', 'request'))
saml2_user_authentication_failed = Signal(providing_args=('request', 'username', 'reason'))

View File

@ -3,7 +3,7 @@ import copy
from urllib import parse
from django.views import View
from django.contrib import auth as auth
from django.contrib import auth
from django.urls import reverse
from django.conf import settings
from django.views.decorators.csrf import csrf_exempt

View File

@ -7,16 +7,7 @@ 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
from .signals import post_auth_success, post_auth_failed, user_auth_failed, user_auth_success
@receiver(user_logged_in)
@ -29,7 +20,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:
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:
@ -44,43 +36,19 @@ def on_user_auth_login_success(sender, user, request, **kwargs):
request.session['auth_session_expiration_required'] = 1
@receiver(openid_user_login_success)
def on_oidc_user_login_success(sender, request, user, create=False, **kwargs):
request.session['auth_backend'] = settings.AUTH_BACKEND_OIDC_CODE
post_auth_success.send(sender, user=user, request=request)
@receiver(openid_user_login_failed)
def on_oidc_user_login_failed(sender, username, request, reason, **kwargs):
request.session['auth_backend'] = settings.AUTH_BACKEND_OIDC_CODE
post_auth_failed.send(sender, username=username, request=request, reason=reason)
@receiver(cas_user_authenticated)
def on_cas_user_login_success(sender, request, user, **kwargs):
request.session['auth_backend'] = settings.AUTH_BACKEND_CAS
post_auth_success.send(sender, user=user, request=request)
@receiver(saml2_user_authenticated)
def on_saml2_user_login_success(sender, request, user, **kwargs):
request.session['auth_backend'] = settings.AUTH_BACKEND_SAML2
@receiver(user_auth_success)
def on_user_login_success(sender, request, user, backend, create=False, **kwargs):
request.session['auth_backend'] = backend
post_auth_success.send(sender, user=user, request=request)
@receiver(saml2_user_authentication_failed)
def on_saml2_user_login_failed(sender, request, username, reason, **kwargs):
request.session['auth_backend'] = settings.AUTH_BACKEND_SAML2
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
@receiver(user_auth_failed)
def on_user_login_failed(sender, username, request, reason, backend, **kwargs):
request.session['auth_backend'] = backend
post_auth_failed.send(sender, username=username, request=request, reason=reason)

View File

@ -3,3 +3,7 @@ from django.dispatch import Signal
post_auth_success = Signal(providing_args=('user', 'request'))
post_auth_failed = Signal(providing_args=('username', 'request', 'reason'))
user_auth_success = Signal(providing_args=('user', 'request', 'backend', 'create'))
user_auth_failed = Signal(providing_args=('username', 'request', 'reason', 'backend'))

View File

@ -79,6 +79,9 @@ function doRequestAuth() {
requestApi({
url: url,
method: "GET",
headers: {
"X-JMS-LOGIN-TYPE": "W"
},
success: function (data) {
if (!data.error && data.msg === 'ok') {
window.onbeforeunload = function(){};
@ -98,9 +101,6 @@ function doRequestAuth() {
},
error: function (text, data) {
},
beforeSend: function(request) {
request.setRequestHeader("X-JMS-LOGIN-TYPE", "W");
},
flash_message: false, // 是否显示flash消息
})
}

View File

@ -330,6 +330,8 @@ class UserLogoutView(TemplateView):
return settings.CAS_LOGOUT_URL_NAME
elif 'saml2' in backend:
return settings.SAML2_LOGOUT_URL_NAME
elif 'oauth2' in backend:
return settings.AUTH_OAUTH2_LOGOUT_URL_NAME
return None
def get(self, request, *args, **kwargs):

View File

@ -10,5 +10,5 @@ celery_task_pre_key = "CELERY_"
KEY_CACHE_RESOURCE_IDS = "RESOURCE_IDS_{}"
# AD User AccountDisable
# https://blog.csdn.net/bytxl/article/details/17763975
# https://docs.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties
LDAP_AD_ACCOUNT_DISABLE = 2

View File

View File

@ -0,0 +1,7 @@
from .device import Device
def open_piico_device(driver_path) -> Device:
d = Device()
d.open(driver_path)
return d

View File

@ -0,0 +1,59 @@
cipher_alg_id = {
"sm4_ebc": 0x00000401,
"sm4_cbc": 0x00000402,
}
class ECCCipher:
def __init__(self, session, public_key, private_key):
self._session = session
self.public_key = public_key
self.private_key = private_key
def encrypt(self, plain_text):
return self._session.ecc_encrypt(self.public_key, plain_text, 0x00020800)
def decrypt(self, cipher_text):
return self._session.ecc_decrypt(self.private_key, cipher_text, 0x00020800)
class EBCCipher:
def __init__(self, session, key_val):
self._session = session
self._key = self.__get_key(key_val)
self._alg = "sm4_ebc"
self._iv = None
def __get_key(self, key_val):
key_val = self.__padding(key_val)
return self._session.import_key(key_val)
@staticmethod
def __padding(val):
# padding
val = bytes(val)
while len(val) % 16 != 0:
val += b'\0'
return val
def encrypt(self, plain_text):
plain_text = self.__padding(plain_text)
cipher_text = self._session.encrypt(plain_text, self._key, cipher_alg_id[self._alg], self._iv)
return bytes(cipher_text)
def decrypt(self, cipher_text):
plain_text = self._session.decrypt(cipher_text, self._key, cipher_alg_id[self._alg], self._iv)
return bytes(plain_text)
def destroy(self):
self._session.destroy_cipher_key(self._key)
self._session.close()
class CBCCipher(EBCCipher):
def __init__(self, session, key, iv):
super().__init__(session, key)
self._iv = iv
self._alg = "sm4_cbc"

View File

@ -0,0 +1,70 @@
from ctypes import *
from .exception import PiicoError
from .session import Session
from .cipher import *
from .digest import *
class Device:
_driver = None
__device = None
def open(self, driver_path="./libpiico_ccmu.so"):
# load driver
self.__load_driver(driver_path)
# open device
self.__open_device()
def close(self):
if self.__device is None:
raise Exception("device not turned on")
ret = self._driver.SDF_CloseDevice(self.__device)
if ret != 0:
raise Exception("turn off device failed")
self.__device = None
def new_session(self):
session = c_void_p()
ret = self._driver.SDF_OpenSession(self.__device, pointer(session))
if ret != 0:
raise Exception("create session failed")
return Session(self._driver, session)
def generate_ecc_key_pair(self):
session = self.new_session()
return session.generate_ecc_key_pair(alg_id=0x00020200)
def generate_random(self, length=64):
session = self.new_session()
return session.generate_random(length)
def new_sm2_ecc_cipher(self, public_key, private_key):
session = self.new_session()
return ECCCipher(session, public_key, private_key)
def new_sm4_ebc_cipher(self, key_val):
session = self.new_session()
return EBCCipher(session, key_val)
def new_sm4_cbc_cipher(self, key_val, iv):
session = self.new_session()
return CBCCipher(session, key_val, iv)
def new_digest(self, mode="sm3"):
session = self.new_session()
return Digest(session, mode)
def __load_driver(self, path):
# check driver status
if self._driver is not None:
raise Exception("already load driver")
# load driver
self._driver = cdll.LoadLibrary(path)
def __open_device(self):
device = c_void_p()
ret = self._driver.SDF_OpenDevice(pointer(device))
if ret != 0:
raise PiicoError("open piico device failed", ret)
self.__device = device

View File

@ -0,0 +1,32 @@
hash_alg_id = {
"sm3": 0x00000001,
"sha1": 0x00000002,
"sha256": 0x00000004,
"sha512": 0x00000008,
}
class Digest:
def __init__(self, session, alg_name="sm3"):
if hash_alg_id[alg_name] is None:
raise Exception("unsupported hash alg {}".format(alg_name))
self._alg_name = alg_name
self._session = session
self.__init_hash()
def __init_hash(self):
self._session.hash_init(hash_alg_id[self._alg_name])
def update(self, data):
self._session.hash_update(data)
def final(self):
return self._session.hash_final()
def reset(self):
self.__init_hash()
def destroy(self):
self._session.close()

View File

@ -0,0 +1,71 @@
from ctypes import *
ECCref_MAX_BITS = 512
ECCref_MAX_LEN = int((ECCref_MAX_BITS + 7) / 8)
class EncodeMixin:
def encode(self):
raise NotImplementedError
class ECCrefPublicKey(Structure, EncodeMixin):
_fields_ = [
('bits', c_uint),
('x', c_ubyte * ECCref_MAX_LEN),
('y', c_ubyte * ECCref_MAX_LEN),
]
def encode(self):
return bytes([0x04]) + bytes(self.x[32:]) + bytes(self.y[32:])
class ECCrefPrivateKey(Structure, EncodeMixin):
_fields_ = [
('bits', c_uint,),
('K', c_ubyte * ECCref_MAX_LEN),
]
def encode(self):
return bytes(self.K[32:])
class ECCCipherEncode(EncodeMixin):
def __init__(self):
self.x = None
self.y = None
self.M = None
self.C = None
self.L = None
def encode(self):
c1 = bytes(self.x[32:]) + bytes(self.y[32:])
c2 = bytes(self.C[:self.L])
c3 = bytes(self.M)
return bytes([0x04]) + c1 + c2 + c3
def new_ecc_cipher_cla(length):
_cache = {}
cla_name = "ECCCipher{}".format(length)
if _cache.__contains__(cla_name):
return _cache[cla_name]
else:
cla = type(cla_name, (Structure, ECCCipherEncode), {
"_fields_": [
('x', c_ubyte * ECCref_MAX_LEN),
('y', c_ubyte * ECCref_MAX_LEN),
('M', c_ubyte * 32),
('L', c_uint),
('C', c_ubyte * length)
]
})
_cache[cla_name] = cla
return cla
class ECCKeyPair:
def __init__(self, public_key, private_key):
self.public_key = public_key
self.private_key = private_key

View File

@ -0,0 +1,12 @@
class PiicoError(Exception):
def __init__(self, msg, ret):
super().__init__(self)
self.__ret = ret
self.__msg = msg
def __str__(self):
return "piico error: {} return code: {}".format(self.__msg, self.hex_ret(self.__ret))
@staticmethod
def hex_ret(ret):
return hex(ret & ((1 << 32) - 1))

View File

@ -0,0 +1,36 @@
from ctypes import *
from .ecc import ECCrefPublicKey, ECCrefPrivateKey, ECCKeyPair
from .exception import PiicoError
from .session_mixin import SM3Mixin, SM4Mixin, SM2Mixin
class Session(SM2Mixin, SM3Mixin, SM4Mixin):
def __init__(self, driver, session):
super().__init__()
self._session = session
self._driver = driver
def get_device_info(self):
pass
def generate_random(self, length=64):
random_data = (c_ubyte * length)()
ret = self._driver.SDF_GenerateRandom(self._session, c_int(length), random_data)
if ret != 0:
raise PiicoError("generate random error", ret)
return bytes(random_data)
def generate_ecc_key_pair(self, alg_id):
public_key = ECCrefPublicKey()
private_key = ECCrefPrivateKey()
ret = self._driver.SDF_GenerateKeyPair_ECC(self._session, c_int(alg_id), c_int(256), pointer(public_key),
pointer(private_key))
if ret != 0:
raise PiicoError("generate ecc key pair failed", ret)
return ECCKeyPair(public_key.encode(), private_key.encode())
def close(self):
ret = self._driver.SDF_CloseSession(self._session)
if ret != 0:
raise PiicoError("close session failed", ret)

View File

@ -0,0 +1,129 @@
from .ecc import *
from .exception import PiicoError
class BaseMixin:
def __init__(self):
self._driver = None
self._session = None
class SM2Mixin(BaseMixin):
def ecc_encrypt(self, public_key, plain_text, alg_id):
pos = 1
k1 = bytes([0] * 32) + bytes(public_key[pos:pos + 32])
k1 = (c_ubyte * len(k1))(*k1)
pos += 32
k2 = bytes([0] * 32) + bytes(public_key[pos:pos + 32])
pk = ECCrefPublicKey(c_uint(0x40), (c_ubyte * len(k1))(*k1), (c_ubyte * len(k2))(*k2))
plain_text = (c_ubyte * len(plain_text))(*plain_text)
ecc_data = new_ecc_cipher_cla(len(plain_text))()
ret = self._driver.SDF_ExternalEncrypt_ECC(self._session, c_int(alg_id), pointer(pk), plain_text,
c_int(len(plain_text)), pointer(ecc_data))
if ret != 0:
raise Exception("ecc encrypt failed", ret)
return ecc_data.encode()
def ecc_decrypt(self, private_key, cipher_text, alg_id):
k = bytes([0] * 32) + bytes(private_key[:32])
vk = ECCrefPrivateKey(c_uint(0x40), (c_ubyte * len(k))(*k))
pos = 1
# c1
x = bytes([0] * 32) + bytes(cipher_text[pos:pos + 32])
pos += 32
y = bytes([0] * 32) + bytes(cipher_text[pos:pos + 32])
pos += 32
# c2
c = bytes(cipher_text[pos:-32])
l = len(c)
# c3
m = bytes(cipher_text[-32:])
ecc_data = new_ecc_cipher_cla(l)(
(c_ubyte * 64)(*x),
(c_ubyte * 64)(*y),
(c_ubyte * 32)(*m),
c_uint(l),
(c_ubyte * l)(*c),
)
temp_data = (c_ubyte * l)()
temp_data_length = c_int()
ret = self._driver.SDF_ExternalDecrypt_ECC(self._session, c_int(alg_id), pointer(vk),
pointer(ecc_data),
temp_data, pointer(temp_data_length))
if ret != 0:
raise Exception("ecc decrypt failed", ret)
return bytes(temp_data[:temp_data_length.value])
class SM3Mixin(BaseMixin):
def hash_init(self, alg_id):
ret = self._driver.SDF_HashInit(self._session, c_int(alg_id), None, None, c_int(0))
if ret != 0:
raise PiicoError("hash init failed,alg id is {}".format(alg_id), ret)
def hash_update(self, data):
data = (c_ubyte * len(data))(*data)
ret = self._driver.SDF_HashUpdate(self._session, data, c_int(len(data)))
if ret != 0:
raise PiicoError("hash update failed", ret)
def hash_final(self):
result_data = (c_ubyte * 32)()
result_length = c_int()
ret = self._driver.SDF_HashFinal(self._session, result_data, pointer(result_length))
if ret != 0:
raise PiicoError("hash final failed", ret)
return bytes(result_data[:result_length.value])
class SM4Mixin(BaseMixin):
def import_key(self, key_val):
# to c lang
key_val = (c_ubyte * len(key_val))(*key_val)
key = c_void_p()
ret = self._driver.SDF_ImportKey(self._session, key_val, c_int(len(key_val)), pointer(key))
if ret != 0:
raise PiicoError("import key failed", ret)
return key
def destroy_cipher_key(self, key):
ret = self._driver.SDF_DestroyKey(self._session, key)
if ret != 0:
raise Exception("destroy key failed")
def encrypt(self, plain_text, key, alg, iv=None):
return self.__do_cipher_action(plain_text, key, alg, iv, True)
def decrypt(self, cipher_text, key, alg, iv=None):
return self.__do_cipher_action(cipher_text, key, alg, iv, False)
def __do_cipher_action(self, text, key, alg, iv=None, encrypt=True):
text = (c_ubyte * len(text))(*text)
if iv is not None:
iv = (c_ubyte * len(iv))(*iv)
temp_data = (c_ubyte * len(text))()
temp_data_length = c_int()
if encrypt:
ret = self._driver.SDF_Encrypt(self._session, key, c_int(alg), iv, text, c_int(len(text)), temp_data,
pointer(temp_data_length))
if ret != 0:
raise PiicoError("encrypt failed", ret)
else:
ret = self._driver.SDF_Decrypt(self._session, key, c_int(alg), iv, text, c_int(len(text)), temp_data,
pointer(temp_data_length))
if ret != 0:
raise PiicoError("decrypt failed", ret)
return temp_data[:temp_data_length.value]

View File

View File

@ -15,6 +15,7 @@ logger = get_logger(__name__)
class BACKENDS(TextChoices):
ALIBABA = 'alibaba', _('Alibaba cloud')
TENCENT = 'tencent', _('Tencent cloud')
HUAWEI = 'huawei', _('Huawei Cloud')
CMPP2 = 'cmpp2', _('CMPP v2.0')

View File

@ -0,0 +1,94 @@
import base64
import hashlib
import time
import uuid
import requests
from collections import OrderedDict
from django.conf import settings
from common.exceptions import JMSException
from common.utils import get_logger
from .base import BaseSMSClient
logger = get_logger(__file__)
class HuaweiClient:
def __init__(self, app_key, app_secret, url, sign_channel_num):
self.url = url[:-1] if url.endswith('/') else url
self.app_key = app_key
self.app_secret = app_secret
self.sign_channel_num = sign_channel_num
def build_wsse_header(self):
now = time.strftime('%Y-%m-%dT%H:%M:%SZ')
nonce = str(uuid.uuid4()).replace('-', '')
digest = hashlib.sha256((nonce + now + self.app_secret).encode()).hexdigest()
digestBase64 = base64.b64encode(digest.encode()).decode()
formatter = 'UsernameToken Username="{}",PasswordDigest="{}",Nonce="{}",Created="{}"'
return formatter.format(self.app_key, digestBase64, nonce, now)
def send_sms(self, receiver, signature, template_id, template_param):
sms_url = '%s/%s' % (self.url, 'sms/batchSendSms/v1')
headers = {
'Authorization': 'WSSE realm="SDP",profile="UsernameToken",type="Appkey"',
'X-WSSE': self.build_wsse_header()
}
body = {
'from': self.sign_channel_num, 'to': receiver, 'templateId': template_id,
'templateParas': template_param, 'signature': signature
}
try:
response = requests.post(sms_url, headers=headers, data=body)
msg = response.json()
except Exception as error:
raise JMSException(code='response_bad', detail=error)
return msg
class HuaweiSMS(BaseSMSClient):
SIGN_AND_TMPL_SETTING_FIELD_PREFIX = 'HUAWEI'
@classmethod
def new_from_settings(cls):
return cls(
app_key=settings.HUAWEI_APP_KEY,
app_secret=settings.HUAWEI_APP_SECRET,
url=settings.HUAWEI_SMS_ENDPOINT,
sign_channel_num=settings.HUAWEI_SIGN_CHANNEL_NUM
)
def __init__(self, app_key: str, app_secret: str, url: str, sign_channel_num: str):
self.client = HuaweiClient(app_key, app_secret, url, sign_channel_num)
def send_sms(
self, phone_numbers: list, sign_name: str, template_code: str,
template_param: OrderedDict, **kwargs
):
phone_numbers_str = ','.join(phone_numbers)
template_param = '["%s"]' % template_param.get('code')
req_params = {
'receiver': phone_numbers_str, 'signature': sign_name,
'template_id': template_code, 'template_param': template_param
}
try:
logger.info(f'Huawei sms send: '
f'phone_numbers={phone_numbers} '
f'sign_name={sign_name} '
f'template_code={template_code} '
f'template_param={template_param}')
resp_msg = self.client.send_sms(**req_params)
except Exception as error:
raise JMSException(code='response_bad', detail=error)
if resp_msg.get('code' != '000000'):
raise JMSException(code='response_bad', detail=resp_msg)
return resp_msg
client = HuaweiSMS

View File

@ -1,15 +1,18 @@
import base64
import logging
import re
from Cryptodome.Cipher import AES, PKCS1_v1_5
from Cryptodome.Random import get_random_bytes
from Cryptodome.PublicKey import RSA
from Cryptodome.Util.Padding import pad
from Cryptodome import Random
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from common.sdk.gm import piico
secret_pattern = re.compile(r'password|secret|key|token', re.IGNORECASE)
@ -63,6 +66,25 @@ class GMSM4EcbCrypto(BaseCrypto):
return self.sm4_decryptor.crypt_ecb(data)
class PiicoSM4EcbCrypto(BaseCrypto):
@staticmethod
def to_16(key):
while len(key) % 16 != 0:
key += b'\0'
return key # 返回bytes
def __init__(self, key, device: piico.Device):
key = padding_key(key, 16)
self.cipher = device.new_sm4_ebc_cipher(key)
def _encrypt(self, data: bytes) -> bytes:
return self.cipher.encrypt(self.to_16(data))
def _decrypt(self, data: bytes) -> bytes:
return self.cipher.decrypt(data)
class AESCrypto:
"""
AES
@ -107,7 +129,15 @@ class AESCryptoGCM:
"""
def __init__(self, key):
self.key = padding_key(key)
self.key = self.process_key(key)
@staticmethod
def process_key(key):
if not isinstance(key, bytes):
key = bytes(key, encoding='utf-8')
if len(key) >= 32:
return key[:32]
return pad(key, 32)
def encrypt(self, text):
"""
@ -155,6 +185,11 @@ def get_gm_sm4_ecb_crypto(key=None):
return GMSM4EcbCrypto(key)
def get_piico_gm_sm4_ecb_crypto(device, key=None):
key = key or settings.SECRET_KEY
return PiicoSM4EcbCrypto(key, device)
aes_ecb_crypto = get_aes_crypto(mode='ECB')
aes_crypto = get_aes_crypto(mode='GCM')
gm_sm4_ecb_crypto = get_gm_sm4_ecb_crypto()
@ -174,10 +209,16 @@ class Crypto:
crypt_algo = settings.SECURITY_DATA_CRYPTO_ALGO
if not crypt_algo:
if settings.GMSSL_ENABLED:
crypt_algo = 'gm'
if settings.PIICO_DEVICE_ENABLE:
piico_driver_path = settings.PIICO_DRIVER_PATH if settings.PIICO_DRIVER_PATH \
else "./lib/libpiico_ccmu.so"
device = piico.open_piico_device(piico_driver_path)
self.cryptor_map["piico_gm"] = get_piico_gm_sm4_ecb_crypto(device)
crypt_algo = 'piico_gm'
else:
crypt_algo = 'gm'
else:
crypt_algo = 'aes'
cryptor = self.cryptor_map.get(crypt_algo, None)
if cryptor is None:
raise ImproperlyConfigured(

View File

@ -224,6 +224,9 @@ class Config(dict):
'CONNECTION_TOKEN_EXPIRATION': 5 * 60,
# Custom Config
'AUTH_CUSTOM': False,
'AUTH_CUSTOM_FILE_MD5': '',
# Auth LDAP settings
'AUTH_LDAP': False,
'AUTH_LDAP_SERVER_URI': 'ldap://localhost:389',
@ -331,8 +334,10 @@ class Config(dict):
'AUTH_OAUTH2_CLIENT_ID': 'client-id',
'AUTH_OAUTH2_SCOPE': '',
'AUTH_OAUTH2_CLIENT_SECRET': '',
'AUTH_OAUTH2_LOGOUT_COMPLETELY': True,
'AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT': 'https://oauth2.example.com/authorize',
'AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT': 'https://oauth2.example.com/userinfo',
'AUTH_OAUTH2_PROVIDER_END_SESSION_ENDPOINT': 'https://oauth2.example.com/logout',
'AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT': 'https://oauth2.example.com/access_token',
'AUTH_OAUTH2_ACCESS_TOKEN_METHOD': 'GET',
'AUTH_OAUTH2_USER_ATTR_MAP': {
@ -376,6 +381,13 @@ class Config(dict):
'TENCENT_VERIFY_SIGN_NAME': '',
'TENCENT_VERIFY_TEMPLATE_CODE': '',
'HUAWEI_APP_KEY': '',
'HUAWEI_APP_SECRET': '',
'HUAWEI_SMS_ENDPOINT': '',
'HUAWEI_SIGN_CHANNEL_NUM': '',
'HUAWEI_VERIFY_SIGN_NAME': '',
'HUAWEI_VERIFY_TEMPLATE_CODE': '',
'CMPP2_HOST': '',
'CMPP2_PORT': 7890,
'CMPP2_SP_ID': '',

View File

@ -2,7 +2,6 @@
#
import os
import ldap
from django.utils.translation import ugettext_lazy as _
from ..const import CONFIG, PROJECT_DIR, BASE_DIR
@ -165,6 +164,7 @@ AUTH_OAUTH2_USER_ATTR_MAP = CONFIG.AUTH_OAUTH2_USER_ATTR_MAP
AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME = 'authentication:oauth2:login-callback'
AUTH_OAUTH2_AUTHENTICATION_REDIRECT_URI = '/'
AUTH_OAUTH2_AUTHENTICATION_FAILURE_REDIRECT_URI = '/'
AUTH_OAUTH2_LOGOUT_URL_NAME = "authentication:oauth2:logout"
# 临时 token
AUTH_TEMP_TOKEN = CONFIG.AUTH_TEMP_TOKEN
@ -195,7 +195,7 @@ AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.sso.AuthorizationTokenAuthent
AUTH_BACKEND_SAML2 = 'authentication.backends.saml2.SAML2Backend'
AUTH_BACKEND_OAUTH2 = 'authentication.backends.oauth2.OAuth2Backend'
AUTH_BACKEND_TEMP_TOKEN = 'authentication.backends.token.TempTokenAuthBackend'
AUTH_BACKEND_CUSTOM = 'authentication.backends.custom.CustomAuthBackend'
AUTHENTICATION_BACKENDS = [
# 只做权限校验
@ -208,9 +208,32 @@ AUTHENTICATION_BACKENDS = [
# 扫码模式
AUTH_BACKEND_WECOM, AUTH_BACKEND_DINGTALK, AUTH_BACKEND_FEISHU,
# Token模式
AUTH_BACKEND_AUTH_TOKEN, AUTH_BACKEND_SSO, AUTH_BACKEND_TEMP_TOKEN
AUTH_BACKEND_AUTH_TOKEN, AUTH_BACKEND_SSO, AUTH_BACKEND_TEMP_TOKEN,
]
def get_file_md5(filepath):
import hashlib
# 创建md5对象
m = hashlib.md5()
with open(filepath, 'rb') as f:
while True:
data = f.read(4096)
if not data:
break
# 更新md5对象
m.update(data)
# 返回md5对象
return m.hexdigest()
AUTH_CUSTOM = CONFIG.AUTH_CUSTOM
AUTH_CUSTOM_FILE_MD5 = CONFIG.AUTH_CUSTOM_FILE_MD5
AUTH_CUSTOM_FILE_PATH = os.path.join(PROJECT_DIR, 'data', 'auth', 'main.py')
if AUTH_CUSTOM and AUTH_CUSTOM_FILE_MD5 == get_file_md5(AUTH_CUSTOM_FILE_PATH):
# 自定义认证模块
AUTHENTICATION_BACKENDS.append(AUTH_BACKEND_CUSTOM)
AUTHENTICATION_BACKENDS_THIRD_PARTY = [AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_CAS, AUTH_BACKEND_SAML2, AUTH_BACKEND_OAUTH2]
ONLY_ALLOW_EXIST_USER_AUTH = CONFIG.ONLY_ALLOW_EXIST_USER_AUTH
ONLY_ALLOW_AUTH_FROM_SOURCE = CONFIG.ONLY_ALLOW_AUTH_FROM_SOURCE

View File

@ -142,6 +142,7 @@ WSGI_APPLICATION = 'jumpserver.wsgi.application'
LOGIN_REDIRECT_URL = reverse_lazy('index')
LOGIN_URL = reverse_lazy('authentication:login')
LOGOUT_REDIRECT_URL = CONFIG.LOGOUT_REDIRECT_URL
SESSION_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN
CSRF_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN

View File

@ -111,6 +111,8 @@ HTTP_LISTEN_PORT = CONFIG.HTTP_LISTEN_PORT
WS_LISTEN_PORT = CONFIG.WS_LISTEN_PORT
LOGIN_LOG_KEEP_DAYS = CONFIG.LOGIN_LOG_KEEP_DAYS
TASK_LOG_KEEP_DAYS = CONFIG.TASK_LOG_KEEP_DAYS
OPERATE_LOG_KEEP_DAYS = CONFIG.OPERATE_LOG_KEEP_DAYS
FTP_LOG_KEEP_DAYS = CONFIG.FTP_LOG_KEEP_DAYS
ORG_CHANGE_TO_URL = CONFIG.ORG_CHANGE_TO_URL
WINDOWS_SKIP_ALL_MANUAL_PASSWORD = CONFIG.WINDOWS_SKIP_ALL_MANUAL_PASSWORD

View File

@ -40,6 +40,7 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
'sms': serializers.SMSSettingSerializer,
'alibaba': serializers.AlibabaSMSSettingSerializer,
'tencent': serializers.TencentSMSSettingSerializer,
'huawei': serializers.HuaweiSMSSettingSerializer,
'cmpp2': serializers.CMPP2SMSSettingSerializer,
}

View File

@ -38,6 +38,7 @@ class SMSTestingAPI(GenericAPIView):
backends_serializer = {
'alibaba': serializers.AlibabaSMSSettingSerializer,
'tencent': serializers.TencentSMSSettingSerializer,
'huawei': serializers.HuaweiSMSSettingSerializer,
'cmpp2': serializers.CMPP2SMSSettingSerializer
}
rbac_perms = {
@ -82,6 +83,22 @@ class SMSTestingAPI(GenericAPIView):
}
return init_params, send_sms_params
def get_huawei_params(self, data):
init_params = {
'app_key': data['HUAWEI_APP_KEY'],
'app_secret': self.get_or_from_setting(
'HUAWEI_APP_SECRET', data.get('HUAWEI_APP_SECRET')
),
'url': data['HUAWEI_SMS_ENDPOINT'],
'sign_channel_num': data['HUAWEI_SIGN_CHANNEL_NUM'],
}
send_sms_params = {
'sign_name': data['HUAWEI_VERIFY_SIGN_NAME'],
'template_code': data['HUAWEI_VERIFY_TEMPLATE_CODE'],
'template_param': OrderedDict(code='666666')
}
return init_params, send_sms_params
def get_cmpp2_params(self, data):
init_params = {
'host': data['CMPP2_HOST'], 'port': data['CMPP2_PORT'],

View File

@ -47,6 +47,10 @@ class OAuth2SettingSerializer(serializers.Serializer):
AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT = serializers.CharField(
required=True, max_length=1024, label=_('Provider userinfo endpoint')
)
AUTH_OAUTH2_PROVIDER_END_SESSION_ENDPOINT = serializers.CharField(
required=False, max_length=1024, label=_('Provider end session endpoint')
)
AUTH_OAUTH2_LOGOUT_COMPLETELY = serializers.BooleanField(required=False, label=_('Logout completely'))
AUTH_OAUTH2_USER_ATTR_MAP = serializers.DictField(
required=True, label=_('User attr map')
)

View File

@ -7,7 +7,7 @@ from common.sdk.sms import BACKENDS
__all__ = [
'SMSSettingSerializer', 'AlibabaSMSSettingSerializer', 'TencentSMSSettingSerializer',
'CMPP2SMSSettingSerializer'
'HuaweiSMSSettingSerializer', 'CMPP2SMSSettingSerializer'
]
@ -52,6 +52,15 @@ class TencentSMSSettingSerializer(BaseSMSSettingSerializer):
TENCENT_VERIFY_TEMPLATE_CODE = serializers.CharField(max_length=256, required=True, label=_('Template code'))
class HuaweiSMSSettingSerializer(BaseSMSSettingSerializer):
HUAWEI_APP_KEY = serializers.CharField(max_length=256, required=True, label='App key')
HUAWEI_APP_SECRET = EncryptedField(max_length=256, required=False, label='App secret')
HUAWEI_SMS_ENDPOINT = serializers.CharField(max_length=1024, required=True, label=_('App Access Address'))
HUAWEI_SIGN_CHANNEL_NUM = serializers.CharField(max_length=1024, required=True, label=_('Signature channel number'))
HUAWEI_VERIFY_SIGN_NAME = serializers.CharField(max_length=256, required=True, label=_('Signature'))
HUAWEI_VERIFY_TEMPLATE_CODE = serializers.CharField(max_length=256, required=True, label=_('Template code'))
class CMPP2SMSSettingSerializer(BaseSMSSettingSerializer):
CMPP2_HOST = serializers.CharField(max_length=256, required=True, label=_('Host'))
CMPP2_PORT = serializers.IntegerField(default=7890, label=_('Port'))

View File

@ -13,7 +13,7 @@ def telnet(dest_addr, port_number=23, timeout=10):
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')
return True, output.decode('utf-8', 'ignore')
if __name__ == "__main__":

View File

@ -270,13 +270,13 @@ function requestApi(props) {
if (typeof(dataBody) === "object") {
dataBody = JSON.stringify(dataBody)
}
var beforeSend = props.beforeSend || function (request) {}
var headers = props.headers || {}
$.ajax({
url: props.url,
type: props.method || "PATCH",
headers: headers,
data: dataBody,
beforeSend: beforeSend,
contentType: props.content_type || "application/json; charset=utf-8",
dataType: props.data_type || "json"
}).done(function (data, textStatue, jqXHR) {

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.13 on 2022-08-24 02:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0039_auto_20211229_1852'),
]
operations = [
migrations.AlterField(
model_name='user',
name='source',
field=models.CharField(choices=[('local', 'Local'), ('ldap', 'LDAP/AD'), ('openid', 'OpenID'), ('radius', 'Radius'), ('cas', 'CAS'), ('saml2', 'SAML2'), ('oauth2', 'OAuth2'), ('custom', 'Custom')], default='local', max_length=30, verbose_name='Source'),
),
]

View File

@ -629,6 +629,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
cas = 'cas', 'CAS'
saml2 = 'saml2', 'SAML2'
oauth2 = 'oauth2', 'OAuth2'
custom = 'custom', 'Custom'
SOURCE_BACKEND_MAPPING = {
Source.local: [
@ -656,6 +657,9 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
Source.oauth2: [
settings.AUTH_BACKEND_OAUTH2
],
Source.custom: [
settings.AUTH_BACKEND_CUSTOM
]
}
id = models.UUIDField(default=uuid.uuid4, primary_key=True)

View File

@ -1,5 +1,5 @@
#!/bin/bash
apt install \
g++ make iputils-ping default-libmysqlclient-dev libpq-dev \
libffi-dev libldap2-dev libsasl2-dev sshpass pkg-config libxml2-dev \
libffi-dev libldap2-dev libsasl2-dev openssh-client sshpass pkg-config libxml2-dev \
libxmlsec1-dev libxmlsec1-openssl libaio-dev freetds-dev

View File

@ -134,9 +134,10 @@ django-mysql==3.9.0
django-redis==5.2.0
python-redis-lock==3.7.0
redis==4.3.3
pymongo==4.2.0
# Debug
ipython==8.4.0
ForgeryPy3==0.3.1
django-debug-toolbar==3.5
Pympler==1.0.1
IPy==1.1
IPy==1.1

View File

@ -1,5 +1,5 @@
#!/bin/bash
yum -y install \
gcc-c++ sshpass mariadb-devel openldap-devel libxml2-devel \
gcc-c++ sshpass mariadb-devel openldap-devel openssh-clients libxml2-devel \
xmlsec1-devel xmlsec1-openssl-devel libtool-ltdl-devel \
postgresql-devel