mirror of https://github.com/jumpserver/jumpserver
feat: 添加短信服务和用户消息通知
parent
d49d1e1414
commit
b1fceca8a6
|
@ -45,5 +45,5 @@ class TicketStatusApi(mixins.AuthMixin, APIView):
|
|||
ticket = self.get_ticket()
|
||||
if ticket:
|
||||
request.session.pop('auth_ticket_id', '')
|
||||
ticket.close(processor=request.user)
|
||||
ticket.close(processor=self.get_user_from_session())
|
||||
return Response('', status=200)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import builtins
|
||||
import time
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.conf import settings
|
||||
|
@ -8,14 +9,28 @@ from rest_framework.generics import CreateAPIView
|
|||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.permissions import IsValidUser, NeedMFAVerify
|
||||
from authentication.sms_verify_code import VerifyCodeUtil
|
||||
from common.exceptions import JMSException
|
||||
from common.permissions import IsValidUser, NeedMFAVerify, IsAppUser
|
||||
from users.models.user import MFAType
|
||||
from ..serializers import OtpVerifySerializer
|
||||
from .. import serializers
|
||||
from .. import errors
|
||||
from ..mixins import AuthMixin
|
||||
|
||||
|
||||
__all__ = ['MFAChallengeApi', 'UserOtpVerifyApi']
|
||||
__all__ = ['MFAChallengeApi', 'UserOtpVerifyApi', 'SendSMSVerifyCodeApi', 'MFASelectTypeApi']
|
||||
|
||||
|
||||
class MFASelectTypeApi(AuthMixin, CreateAPIView):
|
||||
permission_classes = (AllowAny,)
|
||||
serializer_class = serializers.MFASelectTypeSerializer
|
||||
|
||||
def perform_create(self, serializer):
|
||||
mfa_type = serializer.validated_data['type']
|
||||
if mfa_type == MFAType.SMS_CODE:
|
||||
user = self.get_user_from_session()
|
||||
user.send_sms_code()
|
||||
|
||||
|
||||
class MFAChallengeApi(AuthMixin, CreateAPIView):
|
||||
|
@ -26,7 +41,9 @@ class MFAChallengeApi(AuthMixin, CreateAPIView):
|
|||
try:
|
||||
user = self.get_user_from_session()
|
||||
code = serializer.validated_data.get('code')
|
||||
valid = user.check_mfa(code)
|
||||
mfa_type = serializer.validated_data.get('type', MFAType.OTP)
|
||||
|
||||
valid = user.check_mfa(code, mfa_type=mfa_type)
|
||||
if not valid:
|
||||
self.request.session['auth_mfa'] = ''
|
||||
raise errors.MFAFailedError(
|
||||
|
@ -67,3 +84,12 @@ class UserOtpVerifyApi(CreateAPIView):
|
|||
if self.request.method.lower() == 'get' and settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
||||
self.permission_classes = [NeedMFAVerify]
|
||||
return super().get_permissions()
|
||||
|
||||
|
||||
class SendSMSVerifyCodeApi(AuthMixin, CreateAPIView):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
user = self.get_user_from_session()
|
||||
timeout = user.send_sms_code()
|
||||
return Response({'code': 'ok','timeout': timeout})
|
||||
|
|
|
@ -4,9 +4,11 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from authentication import sms_verify_code
|
||||
from common.exceptions import JMSException
|
||||
from .signals import post_auth_failed
|
||||
from users.utils import LoginBlockUtil, MFABlockUtils
|
||||
from users.models import MFAType
|
||||
|
||||
reason_password_failed = 'password_failed'
|
||||
reason_password_decrypt_failed = 'password_decrypt_failed'
|
||||
|
@ -58,8 +60,18 @@ block_mfa_msg = _(
|
|||
"The account has been locked "
|
||||
"(please contact admin to unlock it or try again after {} minutes)"
|
||||
)
|
||||
mfa_failed_msg = _(
|
||||
"MFA code invalid, or ntp sync server time, "
|
||||
otp_failed_msg = _(
|
||||
"One-time password invalid, or ntp sync server time, "
|
||||
"You can also try {times_try} times "
|
||||
"(The account will be temporarily locked for {block_time} minutes)"
|
||||
)
|
||||
sms_failed_msg = _(
|
||||
"SMS verify code invalid,"
|
||||
"You can also try {times_try} times "
|
||||
"(The account will be temporarily locked for {block_time} minutes)"
|
||||
)
|
||||
mfa_type_failed_msg = _(
|
||||
"The MFA type({mfa_type}) is not supported"
|
||||
"You can also try {times_try} times "
|
||||
"(The account will be temporarily locked for {block_time} minutes)"
|
||||
)
|
||||
|
@ -134,7 +146,7 @@ class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError):
|
|||
error = reason_mfa_failed
|
||||
msg: str
|
||||
|
||||
def __init__(self, username, request, ip):
|
||||
def __init__(self, username, request, ip, mfa_type=MFAType.OTP):
|
||||
util = MFABlockUtils(username, ip)
|
||||
util.incr_failed_count()
|
||||
|
||||
|
@ -142,9 +154,18 @@ class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError):
|
|||
block_time = settings.SECURITY_LOGIN_LIMIT_TIME
|
||||
|
||||
if times_remainder:
|
||||
self.msg = mfa_failed_msg.format(
|
||||
times_try=times_remainder, block_time=block_time
|
||||
)
|
||||
if mfa_type == MFAType.OTP:
|
||||
self.msg = otp_failed_msg.format(
|
||||
times_try=times_remainder, block_time=block_time
|
||||
)
|
||||
elif mfa_type == MFAType.SMS_CODE:
|
||||
self.msg = sms_failed_msg.format(
|
||||
times_try=times_remainder, block_time=block_time
|
||||
)
|
||||
else:
|
||||
self.msg = mfa_type_failed_msg.format(
|
||||
mfa_type=mfa_type, times_try=times_remainder, block_time=block_time
|
||||
)
|
||||
else:
|
||||
self.msg = block_mfa_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
|
||||
super().__init__(username=username, request=request)
|
||||
|
@ -202,12 +223,16 @@ class MFARequiredError(NeedMoreInfoError):
|
|||
msg = mfa_required_msg
|
||||
error = 'mfa_required'
|
||||
|
||||
def __init__(self, error='', msg='', mfa_types=tuple(MFAType)):
|
||||
super().__init__(error=error, msg=msg)
|
||||
self.choices = mfa_types
|
||||
|
||||
def as_data(self):
|
||||
return {
|
||||
'error': self.error,
|
||||
'msg': self.msg,
|
||||
'data': {
|
||||
'choices': ['code'],
|
||||
'choices': self.choices,
|
||||
'url': reverse('api-auth:mfa-challenge')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,8 @@ class UserLoginForm(forms.Form):
|
|||
|
||||
|
||||
class UserCheckOtpCodeForm(forms.Form):
|
||||
otp_code = forms.CharField(label=_('MFA code'), max_length=6)
|
||||
code = forms.CharField(label=_('Code'), max_length=6)
|
||||
mfa_type = forms.CharField(label=_('MFA type'), max_length=6)
|
||||
|
||||
|
||||
class CustomCaptchaTextInput(CaptchaTextInput):
|
||||
|
|
|
@ -17,7 +17,7 @@ from django.shortcuts import reverse, redirect
|
|||
from django.views.generic.edit import FormView
|
||||
|
||||
from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get, FlashMessageUtil
|
||||
from users.models import User
|
||||
from users.models import User, MFAType
|
||||
from users.utils import LoginBlockUtil, MFABlockUtils
|
||||
from . import errors
|
||||
from .utils import rsa_decrypt, gen_key_pair
|
||||
|
@ -351,13 +351,13 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
|||
unset, url = user.mfa_enabled_but_not_set()
|
||||
if unset:
|
||||
raise errors.MFAUnsetError(user, self.request, url)
|
||||
raise errors.MFARequiredError()
|
||||
raise errors.MFARequiredError(mfa_types=user.get_supported_mfa_types())
|
||||
|
||||
def mark_mfa_ok(self):
|
||||
def mark_mfa_ok(self, mfa_type=MFAType.OTP):
|
||||
self.request.session['auth_mfa'] = 1
|
||||
self.request.session['auth_mfa_time'] = time.time()
|
||||
self.request.session['auth_mfa_type'] = 'otp'
|
||||
self.request.session['auth_mfa_required'] = ''
|
||||
self.request.session['auth_mfa_type'] = mfa_type
|
||||
|
||||
def check_mfa_is_block(self, username, ip, raise_exception=True):
|
||||
if MFABlockUtils(username, ip).is_block():
|
||||
|
@ -368,11 +368,11 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
|||
else:
|
||||
return exception
|
||||
|
||||
def check_user_mfa(self, code):
|
||||
def check_user_mfa(self, code, mfa_type=MFAType.OTP):
|
||||
user = self.get_user_from_session()
|
||||
ip = self.get_request_ip()
|
||||
self.check_mfa_is_block(user.username, ip)
|
||||
ok = user.check_mfa(code)
|
||||
ok = user.check_mfa(code, mfa_type=mfa_type)
|
||||
if ok:
|
||||
self.mark_mfa_ok()
|
||||
return
|
||||
|
@ -380,7 +380,7 @@ class AuthMixin(PasswordEncryptionViewMixin):
|
|||
raise errors.MFAFailedError(
|
||||
username=user.username,
|
||||
request=self.request,
|
||||
ip=ip
|
||||
ip=ip, mfa_type=mfa_type,
|
||||
)
|
||||
|
||||
def get_ticket(self):
|
||||
|
|
|
@ -17,7 +17,7 @@ __all__ = [
|
|||
'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer',
|
||||
'MFAChallengeSerializer', 'LoginConfirmSettingSerializer', 'SSOTokenSerializer',
|
||||
'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer',
|
||||
'PasswordVerifySerializer',
|
||||
'PasswordVerifySerializer', 'MFASelectTypeSerializer',
|
||||
]
|
||||
|
||||
|
||||
|
@ -77,6 +77,10 @@ class BearerTokenSerializer(serializers.Serializer):
|
|||
return instance
|
||||
|
||||
|
||||
class MFASelectTypeSerializer(serializers.Serializer):
|
||||
type = serializers.CharField()
|
||||
|
||||
|
||||
class MFAChallengeSerializer(serializers.Serializer):
|
||||
type = serializers.CharField(write_only=True, required=False, allow_blank=True)
|
||||
code = serializers.CharField(write_only=True)
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
import random
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.message.backends.sms.alibaba import AlibabaSMS
|
||||
from common.message.backends.sms import SMS
|
||||
from common.utils import get_logger
|
||||
from common.exceptions import JMSException
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class CodeExpired(JMSException):
|
||||
default_code = 'verify_code_expired'
|
||||
default_detail = _('The verification code has expired. Please resend it')
|
||||
|
||||
|
||||
class CodeError(JMSException):
|
||||
default_code = 'verify_code_error'
|
||||
default_detail = _('The verification code is incorrect')
|
||||
|
||||
|
||||
class CodeSendTooFrequently(JMSException):
|
||||
default_code = 'code_send_too_frequently'
|
||||
default_detail = _('Please wait {} seconds before sending')
|
||||
|
||||
def __init__(self, ttl):
|
||||
super().__init__(detail=self.default_detail.format(ttl))
|
||||
|
||||
|
||||
class VerifyCodeUtil:
|
||||
KEY_TMPL = 'auth-verify_code-{}'
|
||||
TIMEOUT = 60
|
||||
|
||||
def __init__(self, account, key_suffix=None, timeout=None):
|
||||
self.account = account
|
||||
self.key_suffix = key_suffix
|
||||
self.code = ''
|
||||
|
||||
if key_suffix is not None:
|
||||
self.key = self.KEY_TMPL.format(key_suffix)
|
||||
else:
|
||||
self.key = self.KEY_TMPL.format(account)
|
||||
self.timeout = self.TIMEOUT if timeout is None else timeout
|
||||
|
||||
def touch(self):
|
||||
"""
|
||||
生成,保存,发送
|
||||
"""
|
||||
ttl = self.ttl()
|
||||
if ttl > 0:
|
||||
raise CodeSendTooFrequently(ttl)
|
||||
|
||||
self.generate()
|
||||
self.save()
|
||||
self.send()
|
||||
|
||||
def generate(self):
|
||||
code = ''.join(random.sample('0123456789', 4))
|
||||
self.code = code
|
||||
return code
|
||||
|
||||
def clear(self):
|
||||
cache.delete(self.key)
|
||||
|
||||
def save(self):
|
||||
cache.set(self.key, self.code, self.timeout)
|
||||
|
||||
def send(self):
|
||||
"""
|
||||
发送信息的方法,如果有错误直接抛出 api 异常
|
||||
"""
|
||||
account = self.account
|
||||
code = self.code
|
||||
|
||||
sms = SMS()
|
||||
sms.send_verify_code(account, code)
|
||||
logger.info(f'Send sms verify code: account={account} code={code}')
|
||||
|
||||
def verify(self, code):
|
||||
right = cache.get(self.key)
|
||||
if not right:
|
||||
raise CodeExpired
|
||||
|
||||
if right != code:
|
||||
raise CodeError
|
||||
|
||||
self.clear()
|
||||
return True
|
||||
|
||||
def ttl(self):
|
||||
return cache.ttl(self.key)
|
||||
|
||||
def get_code(self):
|
||||
return cache.get(self.key)
|
|
@ -9,24 +9,60 @@
|
|||
{% block content %}
|
||||
<form class="m-t" role="form" method="post" action="">
|
||||
{% csrf_token %}
|
||||
{% if 'otp_code' in form.errors %}
|
||||
<p class="red-fonts">{{ form.otp_code.errors.as_text }}</p>
|
||||
{% if 'code' in form.errors %}
|
||||
<p class="red-fonts">{{ form.code.errors.as_text }}</p>
|
||||
{% endif %}
|
||||
<div class="form-group">
|
||||
<select class="form-control">
|
||||
<option value="otp" selected>{% trans 'One-time password' %}</option>
|
||||
<select id="verify-method-select" name="mfa_type" class="form-control" onchange="select_change(this.value)">
|
||||
{% for method in methods %}
|
||||
<option value="{{ method.name }}" {% if method.selected %} selected {% endif %} {% if not method.enable %} disabled {% endif %}>{{ method.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="otp_code" placeholder="" required="" autofocus="autofocus">
|
||||
<input type="text" class="form-control" name="code" placeholder="" required="" autofocus="autofocus">
|
||||
<span class="help-block">
|
||||
{% trans 'Open MFA Authenticator and enter the 6-bit dynamic code' %}
|
||||
{% trans 'Please enter the verification code' %}
|
||||
</span>
|
||||
</div>
|
||||
<button id='send-sms-verify-code' class="btn btn-primary full-width m-b" onclick="sendSMSVerifyCode()" style="display: none">{% trans 'Send verification code' %}</button>
|
||||
<button type="submit" class="btn btn-primary block full-width m-b">{% trans 'Next' %}</button>
|
||||
|
||||
<div>
|
||||
<small>{% trans "Can't provide security? Please contact the administrator!" %}</small>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
|
||||
var methodSelect = document.getElementById('verify-method-select');
|
||||
if (methodSelect.value !== null) {
|
||||
select_change(methodSelect.value);
|
||||
}
|
||||
|
||||
function select_change(type){
|
||||
var currentBtn = document.getElementById('send-sms-verify-code');
|
||||
|
||||
if (type == "sms") {
|
||||
currentBtn.style.display = "block";
|
||||
}
|
||||
else {
|
||||
currentBtn.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function sendSMSVerifyCode(){
|
||||
var url = "{% url 'api-auth:sms-verify-code-send' %}";
|
||||
requestApi({
|
||||
url: url,
|
||||
method: "POST",
|
||||
success: function (data) {
|
||||
alert('验证码已发送');
|
||||
},
|
||||
error: function (text, data) {
|
||||
alert(data.detail)
|
||||
},
|
||||
flash_message: false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -27,7 +27,9 @@ urlpatterns = [
|
|||
path('auth/', api.TokenCreateApi.as_view(), name='user-auth'),
|
||||
path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'),
|
||||
path('mfa/challenge/', api.MFAChallengeApi.as_view(), name='mfa-challenge'),
|
||||
path('mfa/select/', api.MFASelectTypeApi.as_view(), name='mfa-select'),
|
||||
path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'),
|
||||
path('sms/verify-code/send/', api.SendSMSVerifyCodeApi.as_view(), name='sms-verify-code-send'),
|
||||
path('password/verify/', api.UserPasswordVerifyApi.as_view(), name='user-password-verify'),
|
||||
path('login-confirm-ticket/status/', api.TicketStatusApi.as_view(), name='login-confirm-ticket-status'),
|
||||
path('login-confirm-settings/<uuid:user_id>/', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update')
|
||||
|
|
|
@ -4,7 +4,6 @@ import base64
|
|||
from Cryptodome.PublicKey import RSA
|
||||
from Cryptodome.Cipher import PKCS1_v1_5
|
||||
from Cryptodome import Random
|
||||
|
||||
from common.utils import get_logger
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
from __future__ import unicode_literals
|
||||
from django.views.generic.edit import FormView
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.conf import settings
|
||||
from .. import forms, errors, mixins
|
||||
from .utils import redirect_to_guard_view
|
||||
|
||||
|
@ -18,12 +20,14 @@ class UserLoginOtpView(mixins.AuthMixin, FormView):
|
|||
redirect_field_name = 'next'
|
||||
|
||||
def form_valid(self, form):
|
||||
otp_code = form.cleaned_data.get('otp_code')
|
||||
otp_code = form.cleaned_data.get('code')
|
||||
mfa_type = form.cleaned_data.get('mfa_type')
|
||||
|
||||
try:
|
||||
self.check_user_mfa(otp_code)
|
||||
self.check_user_mfa(otp_code, mfa_type)
|
||||
return redirect_to_guard_view()
|
||||
except (errors.MFAFailedError, errors.BlockMFAError) as e:
|
||||
form.add_error('otp_code', e.msg)
|
||||
form.add_error('code', e.msg)
|
||||
return super().form_invalid(form)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
@ -31,3 +35,28 @@ class UserLoginOtpView(mixins.AuthMixin, FormView):
|
|||
traceback.print_exception()
|
||||
return redirect_to_guard_view()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
user = self.get_user_from_session()
|
||||
context = {
|
||||
'methods': [
|
||||
{
|
||||
'name': 'otp',
|
||||
'label': _('One-time password'),
|
||||
'enable': bool(user.otp_secret_key),
|
||||
'selected': False,
|
||||
},
|
||||
{
|
||||
'name': 'sms',
|
||||
'label': _('SMS'),
|
||||
'enable': bool(user.phone) and settings.AUTH_SMS,
|
||||
'selected': False,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
for item in context['methods']:
|
||||
if item['enable']:
|
||||
item['selected'] = True
|
||||
break
|
||||
context.update(kwargs)
|
||||
return context
|
||||
|
|
|
@ -2,9 +2,12 @@ import time
|
|||
import hmac
|
||||
import base64
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.message.backends.utils import digest, as_request
|
||||
from common.message.backends.mixin import BaseRequest
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
def sign(secret, data):
|
||||
|
||||
|
@ -160,6 +163,7 @@ class DingTalk:
|
|||
}
|
||||
}
|
||||
}
|
||||
logger.info(f'Dingtalk send text: user_ids={user_ids} msg={msg}')
|
||||
data = self._request.post(URL.SEND_MESSAGE, json=body, with_token=True)
|
||||
return data
|
||||
|
||||
|
|
|
@ -106,6 +106,7 @@ class FeiShu(RequestMixin):
|
|||
body['receive_id'] = user_id
|
||||
|
||||
try:
|
||||
logger.info(f'Feishu send text: user_ids={user_ids} msg={msg}')
|
||||
self._requests.post(URL.SEND_MESSAGE, params=params, json=body)
|
||||
except APIException as e:
|
||||
# 只处理可预知的错误
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
from collections import OrderedDict
|
||||
import importlib
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.db.models import TextChoices
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.exceptions import JMSException
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class SMS_MESSAGE(TextChoices):
|
||||
"""
|
||||
定义短信的各种消息类型,会存到类似 `ALIBABA_SMS_SIGN_AND_TEMPLATES` settings 里
|
||||
|
||||
{
|
||||
'verification_code': {'sign_name': 'Jumpserver', 'template_code': 'SMS_222870834'},
|
||||
...
|
||||
}
|
||||
"""
|
||||
|
||||
"""
|
||||
验证码签名和模板。模板例子:
|
||||
`您的验证码:${code},您正进行身份验证,打死不告诉别人!`
|
||||
其中必须包含 `code` 变量
|
||||
"""
|
||||
VERIFICATION_CODE = 'verification_code'
|
||||
|
||||
def get_sign_and_tmpl(self, config: dict):
|
||||
try:
|
||||
data = config[self]
|
||||
return data['sign_name'], data['template_code']
|
||||
except KeyError as e:
|
||||
raise JMSException(
|
||||
code=f'{settings.SMS_BACKEND}_sign_and_tmpl_bad',
|
||||
detail=_('SMS sign and template bad: {}').format(e)
|
||||
)
|
||||
|
||||
|
||||
class BACKENDS(TextChoices):
|
||||
ALIBABA = 'alibaba', _('Alibaba')
|
||||
TENCENT = 'tencent', _('Tencent')
|
||||
|
||||
|
||||
class BaseSMSClient:
|
||||
"""
|
||||
短信终端的基类
|
||||
"""
|
||||
|
||||
SIGN_AND_TMPL_SETTING_FIELD: str
|
||||
|
||||
@property
|
||||
def sign_and_tmpl(self):
|
||||
return getattr(settings, self.SIGN_AND_TMPL_SETTING_FIELD, {})
|
||||
|
||||
@classmethod
|
||||
def new_from_settings(cls):
|
||||
raise NotImplementedError
|
||||
|
||||
def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SMS:
|
||||
client: BaseSMSClient
|
||||
|
||||
def __init__(self, backend=None):
|
||||
m = importlib.import_module(f'.{backend or settings.SMS_BACKEND}', __package__)
|
||||
self.client = m.client.new_from_settings()
|
||||
|
||||
def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
|
||||
return self.client.send_sms(
|
||||
phone_numbers=phone_numbers,
|
||||
sign_name=sign_name,
|
||||
template_code=template_code,
|
||||
template_param=template_param,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def send_verify_code(self, phone_number, code):
|
||||
sign_name, template_code = SMS_MESSAGE.VERIFICATION_CODE.get_sign_and_tmpl(self.client.sign_and_tmpl)
|
||||
return self.send_sms([phone_number], sign_name, template_code, OrderedDict(code=code))
|
|
@ -0,0 +1,61 @@
|
|||
import json
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.conf import settings
|
||||
from alibabacloud_dysmsapi20170525.client import Client as Dysmsapi20170525Client
|
||||
from alibabacloud_tea_openapi import models as open_api_models
|
||||
from alibabacloud_dysmsapi20170525 import models as dysmsapi_20170525_models
|
||||
from Tea.exceptions import TeaException
|
||||
|
||||
from common.utils import get_logger
|
||||
from common.exceptions import JMSException
|
||||
from . import BaseSMSClient
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class AlibabaSMS(BaseSMSClient):
|
||||
SIGN_AND_TMPL_SETTING_FIELD = 'ALIBABA_SMS_SIGN_AND_TEMPLATES'
|
||||
|
||||
@classmethod
|
||||
def new_from_settings(cls):
|
||||
return cls(
|
||||
access_key_id=settings.ALIBABA_ACCESS_KEY_ID,
|
||||
access_key_secret=settings.ALIBABA_ACCESS_KEY_SECRET
|
||||
)
|
||||
|
||||
def __init__(self, access_key_id: str, access_key_secret: str):
|
||||
config = open_api_models.Config(
|
||||
# 您的AccessKey ID,
|
||||
access_key_id=access_key_id,
|
||||
# 您的AccessKey Secret,
|
||||
access_key_secret=access_key_secret
|
||||
)
|
||||
# 访问的域名
|
||||
config.endpoint = 'dysmsapi.aliyuncs.com'
|
||||
self.client = Dysmsapi20170525Client(config)
|
||||
|
||||
def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
|
||||
phone_numbers_str = ','.join(phone_numbers)
|
||||
send_sms_request = dysmsapi_20170525_models.SendSmsRequest(
|
||||
phone_numbers=phone_numbers_str, sign_name=sign_name,
|
||||
template_code=template_code, template_param=json.dumps(template_param)
|
||||
)
|
||||
try:
|
||||
logger.info(f'Alibaba sms send: '
|
||||
f'phone_numbers={phone_numbers} '
|
||||
f'sign_name={sign_name} '
|
||||
f'template_code={template_code} '
|
||||
f'template_param={template_param}')
|
||||
response = self.client.send_sms(send_sms_request)
|
||||
# 这里只判断是否成功,失败抛出异常
|
||||
if response.body.code != 'OK':
|
||||
raise JMSException(detail=response.body.message, code=response.body.code)
|
||||
except TeaException as e:
|
||||
if e.code == 'SignatureDoesNotMatch':
|
||||
raise JMSException(code=e.code, detail=_('Signature does not match'))
|
||||
raise JMSException(code=e.code, detail=e.message)
|
||||
return response
|
||||
|
||||
|
||||
client = AlibabaSMS
|
|
@ -0,0 +1,90 @@
|
|||
import json
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from common.exceptions import JMSException
|
||||
from common.utils import get_logger
|
||||
from tencentcloud.common import credential
|
||||
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
|
||||
# 导入对应产品模块的client models。
|
||||
from tencentcloud.sms.v20210111 import sms_client, models
|
||||
# 导入可选配置类
|
||||
from tencentcloud.common.profile.client_profile import ClientProfile
|
||||
from tencentcloud.common.profile.http_profile import HttpProfile
|
||||
from . import BaseSMSClient
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class TencentSMS(BaseSMSClient):
|
||||
"""
|
||||
https://cloud.tencent.com/document/product/382/43196#.E5.8F.91.E9.80.81.E7.9F.AD.E4.BF.A1
|
||||
"""
|
||||
SIGN_AND_TMPL_SETTING_FIELD = 'TENCENT_SMS_SIGN_AND_TEMPLATES'
|
||||
|
||||
@classmethod
|
||||
def new_from_settings(cls):
|
||||
return cls(
|
||||
secret_id=settings.TENCENT_SECRET_ID,
|
||||
secret_key=settings.TENCENT_SECRET_KEY,
|
||||
sdkappid=settings.TENCENT_SDKAPPID
|
||||
)
|
||||
|
||||
def __init__(self, secret_id: str, secret_key: str, sdkappid: str):
|
||||
self.sdkappid = sdkappid
|
||||
|
||||
cred = credential.Credential(secret_id, secret_key)
|
||||
httpProfile = HttpProfile()
|
||||
httpProfile.reqMethod = "POST" # post请求(默认为post请求)
|
||||
httpProfile.reqTimeout = 30 # 请求超时时间,单位为秒(默认60秒)
|
||||
httpProfile.endpoint = "sms.tencentcloudapi.com"
|
||||
|
||||
clientProfile = ClientProfile()
|
||||
clientProfile.signMethod = "TC3-HMAC-SHA256" # 指定签名算法
|
||||
clientProfile.language = "en-US"
|
||||
clientProfile.httpProfile = httpProfile
|
||||
self.client = sms_client.SmsClient(cred, "ap-guangzhou", clientProfile)
|
||||
|
||||
def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: OrderedDict, **kwargs):
|
||||
try:
|
||||
req = models.SendSmsRequest()
|
||||
# 基本类型的设置:
|
||||
# SDK采用的是指针风格指定参数,即使对于基本类型你也需要用指针来对参数赋值。
|
||||
# SDK提供对基本类型的指针引用封装函数
|
||||
# 帮助链接:
|
||||
# 短信控制台: https://console.cloud.tencent.com/smsv2
|
||||
# sms helper: https://cloud.tencent.com/document/product/382/3773
|
||||
|
||||
# 短信应用ID: 短信SdkAppId在 [短信控制台] 添加应用后生成的实际SdkAppId,示例如1400006666
|
||||
req.SmsSdkAppId = self.sdkappid
|
||||
# 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名,签名信息可登录 [短信控制台] 查看
|
||||
req.SignName = sign_name
|
||||
# 短信码号扩展号: 默认未开通,如需开通请联系 [sms helper]
|
||||
req.ExtendCode = ""
|
||||
# 用户的 session 内容: 可以携带用户侧 ID 等上下文信息,server 会原样返回
|
||||
req.SessionContext = "Jumpserver"
|
||||
# 国际/港澳台短信 senderid: 国内短信填空,默认未开通,如需开通请联系 [sms helper]
|
||||
req.SenderId = ""
|
||||
# 下发手机号码,采用 E.164 标准,+[国家或地区码][手机号]
|
||||
# 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号
|
||||
req.PhoneNumberSet = phone_numbers
|
||||
# 模板 ID: 必须填写已审核通过的模板 ID。模板ID可登录 [短信控制台] 查看
|
||||
req.TemplateId = template_code
|
||||
# 模板参数: 若无模板参数,则设置为空
|
||||
req.TemplateParamSet = list(template_param.values())
|
||||
# 通过client对象调用DescribeInstances方法发起请求。注意请求方法名与请求对象是对应的。
|
||||
# 返回的resp是一个DescribeInstancesResponse类的实例,与请求对象对应。
|
||||
logger.info(f'Tencent sms send: '
|
||||
f'phone_numbers={phone_numbers} '
|
||||
f'sign_name={sign_name} '
|
||||
f'template_code={template_code} '
|
||||
f'template_param={template_param}')
|
||||
|
||||
resp = self.client.SendSms(req)
|
||||
|
||||
return resp
|
||||
except TencentCloudSDKException as e:
|
||||
raise JMSException(code=e.code, detail=e.message)
|
||||
|
||||
|
||||
client = TencentSMS
|
|
@ -115,6 +115,7 @@ class WeCom(RequestMixin):
|
|||
},
|
||||
**extra_params
|
||||
}
|
||||
logger.info(f'Wecom send text: users={users} msg={msg}')
|
||||
data = self._requests.post(URL.SEND_MESSAGE, json=body, check_errcode_is_0=False)
|
||||
|
||||
errcode = data['errcode']
|
||||
|
|
|
@ -191,3 +191,11 @@ class HasQueryParamsUserAndIsCurrentOrgMember(permissions.BasePermission):
|
|||
return False
|
||||
query_user = current_org.get_members().filter(id=query_user_id).first()
|
||||
return bool(query_user)
|
||||
|
||||
|
||||
class OnlySuperUserCanList(IsValidUser):
|
||||
def has_permission(self, request, view):
|
||||
user = request.user
|
||||
if view.action == 'list' and not user.is_superuser:
|
||||
return False
|
||||
return True
|
||||
|
|
|
@ -243,6 +243,19 @@ class Config(dict):
|
|||
'LOGIN_REDIRECT_TO_BACKEND': '', # 'OPENID / CAS
|
||||
'LOGIN_REDIRECT_MSG_ENABLED': True,
|
||||
|
||||
'AUTH_SMS': False,
|
||||
'SMS_BACKEND': '',
|
||||
'SMS_TEST_PHONE': '',
|
||||
|
||||
'ALIBABA_ACCESS_KEY_ID': '',
|
||||
'ALIBABA_ACCESS_KEY_SECRET': '',
|
||||
'ALIBABA_SMS_SIGN_AND_TEMPLATES': {},
|
||||
|
||||
'TENCENT_SECRET_ID': '',
|
||||
'TENCENT_SECRET_KEY': '',
|
||||
'TENCENT_SDKAPPID': '',
|
||||
'TENCENT_SMS_SIGN_AND_TEMPLATES': {},
|
||||
|
||||
'OTP_VALID_WINDOW': 2,
|
||||
'OTP_ISSUER_NAME': 'JumpServer',
|
||||
'EMAIL_SUFFIX': 'example.com',
|
||||
|
|
|
@ -122,6 +122,22 @@ AUTH_FEISHU = CONFIG.AUTH_FEISHU
|
|||
FEISHU_APP_ID = CONFIG.FEISHU_APP_ID
|
||||
FEISHU_APP_SECRET = CONFIG.FEISHU_APP_SECRET
|
||||
|
||||
# SMS auth
|
||||
AUTH_SMS = CONFIG.AUTH_SMS
|
||||
SMS_BACKEND = CONFIG.SMS_BACKEND
|
||||
SMS_TEST_PHONE = CONFIG.SMS_TEST_PHONE
|
||||
|
||||
# Alibaba
|
||||
ALIBABA_ACCESS_KEY_ID = CONFIG.ALIBABA_ACCESS_KEY_ID
|
||||
ALIBABA_ACCESS_KEY_SECRET = CONFIG.ALIBABA_ACCESS_KEY_SECRET
|
||||
ALIBABA_SMS_SIGN_AND_TEMPLATES = CONFIG.ALIBABA_SMS_SIGN_AND_TEMPLATES
|
||||
|
||||
# TENCENT
|
||||
TENCENT_SECRET_ID = CONFIG.TENCENT_SECRET_ID
|
||||
TENCENT_SECRET_KEY = CONFIG.TENCENT_SECRET_KEY
|
||||
TENCENT_SDKAPPID = CONFIG.TENCENT_SDKAPPID
|
||||
TENCENT_SMS_SIGN_AND_TEMPLATES = CONFIG.TENCENT_SMS_SIGN_AND_TEMPLATES
|
||||
|
||||
# Other setting
|
||||
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
|
||||
LOGIN_CONFIRM_ENABLE = CONFIG.LOGIN_CONFIRM_ENABLE
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
from django.http import Http404
|
||||
from rest_framework.mixins import ListModelMixin, UpdateModelMixin
|
||||
from rest_framework.mixins import ListModelMixin, UpdateModelMixin, RetrieveModelMixin
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
from common.drf.api import JMSGenericViewSet
|
||||
from common.permissions import IsObjectOwner, IsSuperUser, OnlySuperUserCanList
|
||||
from notifications.notifications import system_msgs
|
||||
from notifications.models import SystemMsgSubscription
|
||||
from notifications.models import SystemMsgSubscription, UserMsgSubscription
|
||||
from notifications.backends import BACKEND
|
||||
from notifications.serializers import (
|
||||
SystemMsgSubscriptionSerializer, SystemMsgSubscriptionByCategorySerializer
|
||||
SystemMsgSubscriptionSerializer, SystemMsgSubscriptionByCategorySerializer,
|
||||
UserMsgSubscriptionSerializer,
|
||||
)
|
||||
|
||||
__all__ = ('BackendListView', 'SystemMsgSubscriptionViewSet')
|
||||
__all__ = ('BackendListView', 'SystemMsgSubscriptionViewSet', 'UserMsgSubscriptionViewSet')
|
||||
|
||||
|
||||
class BackendListView(APIView):
|
||||
|
@ -70,3 +70,13 @@ class SystemMsgSubscriptionViewSet(ListModelMixin,
|
|||
|
||||
serializer = self.get_serializer(data, many=True)
|
||||
return Response(data=serializer.data)
|
||||
|
||||
|
||||
class UserMsgSubscriptionViewSet(ListModelMixin,
|
||||
RetrieveModelMixin,
|
||||
UpdateModelMixin,
|
||||
JMSGenericViewSet):
|
||||
lookup_field = 'user_id'
|
||||
queryset = UserMsgSubscription.objects.all()
|
||||
serializer_class = UserMsgSubscriptionSerializer
|
||||
permission_classes = (IsObjectOwner | IsSuperUser, OnlySuperUserCanList)
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import importlib
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.db import models
|
||||
|
||||
from .dingtalk import DingTalk
|
||||
from .email import Email
|
||||
from .site_msg import SiteMessage
|
||||
from .wecom import WeCom
|
||||
from .feishu import FeiShu
|
||||
client_name_mapper = {}
|
||||
|
||||
|
||||
class BACKEND(models.TextChoices):
|
||||
|
@ -14,17 +12,11 @@ class BACKEND(models.TextChoices):
|
|||
DINGTALK = 'dingtalk', _('DingTalk')
|
||||
SITE_MSG = 'site_msg', _('Site message')
|
||||
FEISHU = 'feishu', _('FeiShu')
|
||||
SMS = 'sms', _('SMS')
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
client = {
|
||||
self.EMAIL: Email,
|
||||
self.WECOM: WeCom,
|
||||
self.DINGTALK: DingTalk,
|
||||
self.SITE_MSG: SiteMessage,
|
||||
self.FEISHU: FeiShu,
|
||||
}[self]
|
||||
return client
|
||||
return client_name_mapper[self]
|
||||
|
||||
def get_account(self, user):
|
||||
return self.client.get_account(user)
|
||||
|
@ -37,3 +29,8 @@ class BACKEND(models.TextChoices):
|
|||
def filter_enable_backends(cls, backends):
|
||||
enable_backends = [b for b in backends if cls(b).is_enable]
|
||||
return enable_backends
|
||||
|
||||
|
||||
for b in BACKEND:
|
||||
m = importlib.import_module(f'.{b}', __package__)
|
||||
client_name_mapper[b] = m.backend
|
||||
|
|
|
@ -14,6 +14,9 @@ class DingTalk(BackendBase):
|
|||
agentid=settings.DINGTALK_AGENTID
|
||||
)
|
||||
|
||||
def send_msg(self, users, msg):
|
||||
def send_msg(self, users, message, subject=None):
|
||||
accounts, __, __ = self.get_accounts(users)
|
||||
return self.dingtalk.send_text(accounts, msg)
|
||||
return self.dingtalk.send_text(accounts, message)
|
||||
|
||||
|
||||
backend = DingTalk
|
||||
|
|
|
@ -8,7 +8,10 @@ class Email(BackendBase):
|
|||
account_field = 'email'
|
||||
is_enable_field_in_settings = 'EMAIL_HOST_USER'
|
||||
|
||||
def send_msg(self, users, subject, message):
|
||||
def send_msg(self, users, message, subject):
|
||||
from_email = settings.EMAIL_FROM or settings.EMAIL_HOST_USER
|
||||
accounts, __, __ = self.get_accounts(users)
|
||||
send_mail(subject, message, from_email, accounts, html_message=message)
|
||||
|
||||
|
||||
backend = Email
|
||||
|
|
|
@ -14,6 +14,9 @@ class FeiShu(BackendBase):
|
|||
app_secret=settings.FEISHU_APP_SECRET
|
||||
)
|
||||
|
||||
def send_msg(self, users, msg):
|
||||
def send_msg(self, users, message, subject=None):
|
||||
accounts, __, __ = self.get_accounts(users)
|
||||
return self.client.send_text(accounts, msg)
|
||||
return self.client.send_text(accounts, message)
|
||||
|
||||
|
||||
backend = FeiShu
|
||||
|
|
|
@ -5,10 +5,13 @@ from .base import BackendBase
|
|||
class SiteMessage(BackendBase):
|
||||
account_field = 'id'
|
||||
|
||||
def send_msg(self, users, subject, message):
|
||||
def send_msg(self, users, message, subject):
|
||||
accounts, __, __ = self.get_accounts(users)
|
||||
Client.send_msg(subject, message, user_ids=accounts)
|
||||
|
||||
@classmethod
|
||||
def is_enable(cls):
|
||||
return True
|
||||
|
||||
|
||||
backend = SiteMessage
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
from django.conf import settings
|
||||
|
||||
from common.message.backends.sms.alibaba import AlibabaSMS as Client
|
||||
from .base import BackendBase
|
||||
|
||||
|
||||
class SMS(BackendBase):
|
||||
account_field = 'phone'
|
||||
is_enable_field_in_settings = 'AUTH_SMS'
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
暂时只对接阿里,之后再扩展
|
||||
"""
|
||||
self.client = Client(
|
||||
access_key_id=settings.ALIBABA_ACCESS_KEY_ID,
|
||||
access_key_secret=settings.ALIBABA_ACCESS_KEY_SECRET
|
||||
)
|
||||
|
||||
def send_msg(self, users, sign_name: str, template_code: str, template_param: dict):
|
||||
accounts, __, __ = self.get_accounts(users)
|
||||
return self.client.send_sms(accounts, sign_name, template_code, template_param)
|
||||
|
||||
|
||||
backend = SMS
|
|
@ -15,6 +15,9 @@ class WeCom(BackendBase):
|
|||
agentid=settings.WECOM_AGENTID
|
||||
)
|
||||
|
||||
def send_msg(self, users, msg):
|
||||
def send_msg(self, users, message, subject=None):
|
||||
accounts, __, __ = self.get_accounts(users)
|
||||
return self.wecom.send_text(accounts, msg)
|
||||
return self.wecom.send_text(accounts, message)
|
||||
|
||||
|
||||
backend = WeCom
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 3.1.12 on 2021-08-23 08:19
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('notifications', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='usermsgsubscription',
|
||||
name='message_type',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='usermsgsubscription',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_msg_subscriptions', to=settings.AUTH_USER_MODEL, unique=True),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,43 @@
|
|||
# Generated by Django 3.1.12 on 2021-08-23 07:52
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def init_user_msg_subscription(apps, schema_editor):
|
||||
UserMsgSubscription = apps.get_model('notifications', 'UserMsgSubscription')
|
||||
User = apps.get_model('users', 'User')
|
||||
|
||||
to_create = []
|
||||
users = User.objects.all()
|
||||
for user in users:
|
||||
receive_backends = []
|
||||
|
||||
receive_backends.append('site_msg')
|
||||
|
||||
if user.email:
|
||||
receive_backends.append('email')
|
||||
|
||||
if user.wecom_id:
|
||||
receive_backends.append('wecom')
|
||||
|
||||
if user.dingtalk_id:
|
||||
receive_backends.append('dingtalk')
|
||||
|
||||
if user.feishu_id:
|
||||
receive_backends.append('feishu')
|
||||
|
||||
to_create.append(UserMsgSubscription(user=user, receive_backends=receive_backends))
|
||||
UserMsgSubscription.objects.bulk_create(to_create)
|
||||
print(f'\n Init user message subscription: {len(to_create)}')
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0036_user_feishu_id'),
|
||||
('notifications', '0002_auto_20210823_1619'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(init_user_msg_subscription)
|
||||
]
|
|
@ -6,12 +6,11 @@ __all__ = ('SystemMsgSubscription', 'UserMsgSubscription')
|
|||
|
||||
|
||||
class UserMsgSubscription(JMSModel):
|
||||
message_type = models.CharField(max_length=128)
|
||||
user = models.ForeignKey('users.User', related_name='user_msg_subscriptions', on_delete=models.CASCADE)
|
||||
user = models.ForeignKey('users.User', unique=True, related_name='user_msg_subscriptions', on_delete=models.CASCADE)
|
||||
receive_backends = models.JSONField(default=list)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.message_type}'
|
||||
return f'{self.user} subscription: {self.receive_backends}'
|
||||
|
||||
|
||||
class SystemMsgSubscription(JMSModel):
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
from typing import Iterable
|
||||
import traceback
|
||||
from itertools import chain
|
||||
from collections import defaultdict
|
||||
|
||||
from django.db.utils import ProgrammingError
|
||||
from celery import shared_task
|
||||
|
||||
from common.utils import lazyproperty
|
||||
from users.models import User
|
||||
from notifications.backends import BACKEND
|
||||
from .models import SystemMsgSubscription
|
||||
from .models import SystemMsgSubscription, UserMsgSubscription
|
||||
|
||||
__all__ = ('SystemMessage', 'UserMessage')
|
||||
|
||||
|
@ -69,37 +71,49 @@ class Message(metaclass=MessageType):
|
|||
for backend in backends:
|
||||
try:
|
||||
backend = BACKEND(backend)
|
||||
if not backend.is_enable:
|
||||
continue
|
||||
|
||||
get_msg_method = getattr(self, f'get_{backend}_msg', self.get_common_msg)
|
||||
msg = get_msg_method()
|
||||
|
||||
try:
|
||||
msg = get_msg_method()
|
||||
except NotImplementedError:
|
||||
continue
|
||||
|
||||
client = backend.client()
|
||||
|
||||
if isinstance(msg, dict):
|
||||
client.send_msg(users, **msg)
|
||||
else:
|
||||
client.send_msg(users, msg)
|
||||
client.send_msg(users, **msg)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
def get_common_msg(self) -> str:
|
||||
def get_common_msg(self) -> dict:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_dingtalk_msg(self) -> str:
|
||||
@lazyproperty
|
||||
def common_msg(self) -> dict:
|
||||
return self.get_common_msg()
|
||||
|
||||
def get_wecom_msg(self) -> str:
|
||||
return self.get_common_msg()
|
||||
# --------------------------------------------------------------
|
||||
# 支持不同发送消息的方式定义自己的消息内容,比如有些支持 html 标签
|
||||
def get_dingtalk_msg(self) -> dict:
|
||||
return self.common_msg
|
||||
|
||||
def get_wecom_msg(self) -> dict:
|
||||
return self.common_msg
|
||||
|
||||
def get_feishu_msg(self) -> dict:
|
||||
return self.common_msg
|
||||
|
||||
def get_email_msg(self) -> dict:
|
||||
msg = self.get_common_msg()
|
||||
subject = f'{msg[:80]} ...' if len(msg) >= 80 else msg
|
||||
return {
|
||||
'subject': subject,
|
||||
'message': msg
|
||||
}
|
||||
return self.common_msg
|
||||
|
||||
def get_site_msg_msg(self) -> dict:
|
||||
return self.get_email_msg()
|
||||
return self.common_msg
|
||||
|
||||
def get_sms_msg(self) -> dict:
|
||||
raise NotImplementedError
|
||||
# --------------------------------------------------------------
|
||||
|
||||
|
||||
class SystemMessage(Message):
|
||||
|
@ -125,4 +139,16 @@ class SystemMessage(Message):
|
|||
|
||||
|
||||
class UserMessage(Message):
|
||||
pass
|
||||
user: User
|
||||
|
||||
def __init__(self, user):
|
||||
self.user = user
|
||||
|
||||
def publish(self):
|
||||
"""
|
||||
发送消息到每个用户配置的接收方式上
|
||||
"""
|
||||
|
||||
sub = UserMsgSubscription.objects.get(user=self.user)
|
||||
|
||||
self.send_msg([self.user], sub.receive_backends)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from common.drf.serializers import BulkModelSerializer
|
||||
from notifications.models import SystemMsgSubscription
|
||||
from notifications.models import SystemMsgSubscription, UserMsgSubscription
|
||||
|
||||
|
||||
class SystemMsgSubscriptionSerializer(BulkModelSerializer):
|
||||
|
@ -27,3 +27,11 @@ class SystemMsgSubscriptionByCategorySerializer(serializers.Serializer):
|
|||
category = serializers.CharField()
|
||||
category_label = serializers.CharField()
|
||||
children = SystemMsgSubscriptionSerializer(many=True)
|
||||
|
||||
|
||||
class UserMsgSubscriptionSerializer(BulkModelSerializer):
|
||||
receive_backends = serializers.ListField(child=serializers.CharField(), read_only=False)
|
||||
|
||||
class Meta:
|
||||
model = UserMsgSubscription
|
||||
fields = ('user_id', 'receive_backends',)
|
||||
|
|
|
@ -6,14 +6,14 @@ from django.utils.functional import LazyObject
|
|||
from django.db.models.signals import post_save
|
||||
from django.db.models.signals import post_migrate
|
||||
from django.dispatch import receiver
|
||||
from django.db.utils import DEFAULT_DB_ALIAS
|
||||
from django.apps import apps as global_apps
|
||||
from django.apps import AppConfig
|
||||
|
||||
from notifications.backends import BACKEND
|
||||
from users.models import User
|
||||
from common.utils.connection import RedisPubSub
|
||||
from common.utils import get_logger
|
||||
from common.decorator import on_transaction_commit
|
||||
from .models import SiteMessage, SystemMsgSubscription
|
||||
from .models import SiteMessage, SystemMsgSubscription, UserMsgSubscription
|
||||
from .notifications import SystemMessage
|
||||
|
||||
|
||||
|
@ -82,3 +82,13 @@ def create_system_messages(app_config: AppConfig, **kwargs):
|
|||
logger.info(f'Create SystemMsgSubscription: package={app_config.module.__package__} type={message_type}')
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def on_user_post_save(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
receive_backends = []
|
||||
for backend in BACKEND:
|
||||
if backend.get_account(instance):
|
||||
receive_backends.append(backend)
|
||||
UserMsgSubscription.objects.create(user=instance, receive_backends=receive_backends)
|
||||
|
|
|
@ -2,9 +2,12 @@ from django.db.models import F
|
|||
from django.db import transaction
|
||||
|
||||
from common.utils.timezone import now
|
||||
from common.utils import get_logger
|
||||
from users.models import User
|
||||
from .models import SiteMessage as SiteMessageModel, SiteMessageUsers
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class SiteMessageUtil:
|
||||
|
||||
|
@ -14,6 +17,11 @@ class SiteMessageUtil:
|
|||
if not any((user_ids, group_ids, is_broadcast)):
|
||||
raise ValueError('No recipient is specified')
|
||||
|
||||
logger.info(f'Site message send: '
|
||||
f'user_ids={user_ids} '
|
||||
f'group_ids={group_ids} '
|
||||
f'subject={subject} '
|
||||
f'message={message}')
|
||||
with transaction.atomic():
|
||||
site_msg = SiteMessageModel.objects.create(
|
||||
subject=subject, message=message,
|
||||
|
|
|
@ -8,6 +8,7 @@ app_name = 'notifications'
|
|||
|
||||
router = BulkRouter()
|
||||
router.register('system-msg-subscription', api.SystemMsgSubscriptionViewSet, 'system-msg-subscription')
|
||||
router.register('user-msg-subscription', api.UserMsgSubscriptionViewSet, 'user-msg-subscription')
|
||||
router.register('site-message', api.SiteMessageViewSet, 'site-message')
|
||||
|
||||
urlpatterns = [
|
||||
|
|
|
@ -18,7 +18,10 @@ class ServerPerformanceMessage(SystemMessage):
|
|||
self._msg = msg
|
||||
|
||||
def get_common_msg(self):
|
||||
return self._msg
|
||||
return {
|
||||
'subject': self._msg[:80],
|
||||
'message': self._msg
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def post_insert_to_db(cls, subscription: SystemMsgSubscription):
|
||||
|
|
|
@ -5,3 +5,6 @@ from .dingtalk import *
|
|||
from .feishu import *
|
||||
from .public import *
|
||||
from .email import *
|
||||
from .alibaba_sms import *
|
||||
from .tencent_sms import *
|
||||
from .sms import *
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
from rest_framework.views import Response
|
||||
from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.exceptions import APIException
|
||||
from rest_framework import status
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.message.backends.sms import SMS_MESSAGE
|
||||
from common.message.backends.sms.alibaba import AlibabaSMS
|
||||
from settings.models import Setting
|
||||
from common.permissions import IsSuperUser
|
||||
from common.exceptions import JMSException
|
||||
|
||||
from .. import serializers
|
||||
|
||||
|
||||
class AlibabaSMSTestingAPI(GenericAPIView):
|
||||
permission_classes = (IsSuperUser,)
|
||||
serializer_class = serializers.AlibabaSMSSettingSerializer
|
||||
|
||||
def post(self, request):
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
alibaba_access_key_id = serializer.validated_data['ALIBABA_ACCESS_KEY_ID']
|
||||
alibaba_access_key_secret = serializer.validated_data.get('ALIBABA_ACCESS_KEY_SECRET')
|
||||
alibaba_sms_sign_and_tmpl = serializer.validated_data['ALIBABA_SMS_SIGN_AND_TEMPLATES']
|
||||
test_phone = serializer.validated_data.get('SMS_TEST_PHONE')
|
||||
|
||||
if not test_phone:
|
||||
raise JMSException(code='test_phone_required', detail=_('test_phone is required'))
|
||||
|
||||
if not alibaba_access_key_secret:
|
||||
secret = Setting.objects.filter(name='ALIBABA_ACCESS_KEY_SECRET').first()
|
||||
if secret:
|
||||
alibaba_access_key_secret = secret.cleaned_value
|
||||
|
||||
alibaba_access_key_secret = alibaba_access_key_secret or ''
|
||||
|
||||
try:
|
||||
client = AlibabaSMS(
|
||||
access_key_id=alibaba_access_key_id,
|
||||
access_key_secret=alibaba_access_key_secret
|
||||
)
|
||||
sign, tmpl = SMS_MESSAGE.VERIFICATION_CODE.get_sign_and_tmpl(alibaba_sms_sign_and_tmpl)
|
||||
|
||||
client.send_sms(
|
||||
phone_numbers=[test_phone],
|
||||
sign_name=sign,
|
||||
template_code=tmpl,
|
||||
template_param={'code': 'test'}
|
||||
)
|
||||
return Response(status=status.HTTP_200_OK, data={'msg': _('Test success')})
|
||||
except APIException as e:
|
||||
try:
|
||||
error = e.detail['errmsg']
|
||||
except:
|
||||
error = e.detail
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': error})
|
|
@ -34,6 +34,8 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
|
|||
'sso': serializers.SSOSettingSerializer,
|
||||
'clean': serializers.CleaningSerializer,
|
||||
'other': serializers.OtherSettingSerializer,
|
||||
'alibaba': serializers.AlibabaSMSSettingSerializer,
|
||||
'tencent': serializers.TencentSMSSettingSerializer,
|
||||
}
|
||||
|
||||
def get_serializer_class(self):
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
from rest_framework.generics import ListAPIView
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.permissions import IsSuperUser
|
||||
from common.message.backends.sms import BACKENDS
|
||||
from settings.serializers.sms import SMSBackendSerializer
|
||||
|
||||
|
||||
class SMSBackendAPI(ListAPIView):
|
||||
permission_classes = (IsSuperUser,)
|
||||
serializer_class = SMSBackendSerializer
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
data = [
|
||||
{
|
||||
'name': b,
|
||||
'label': b.label
|
||||
}
|
||||
for b in BACKENDS
|
||||
]
|
||||
|
||||
return Response(data)
|
|
@ -0,0 +1,63 @@
|
|||
from collections import OrderedDict
|
||||
|
||||
from rest_framework.views import Response
|
||||
from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.exceptions import APIException
|
||||
from rest_framework import status
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.message.backends.sms import SMS_MESSAGE
|
||||
from common.message.backends.sms.tencent import TencentSMS
|
||||
from settings.models import Setting
|
||||
from common.permissions import IsSuperUser
|
||||
from common.exceptions import JMSException
|
||||
|
||||
from .. import serializers
|
||||
|
||||
|
||||
class TencentSMSTestingAPI(GenericAPIView):
|
||||
permission_classes = (IsSuperUser,)
|
||||
serializer_class = serializers.TencentSMSSettingSerializer
|
||||
|
||||
def post(self, request):
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
tencent_secret_id = serializer.validated_data['TENCENT_SECRET_ID']
|
||||
tencent_secret_key = serializer.validated_data.get('TENCENT_SECRET_KEY')
|
||||
tencent_sms_sign_and_tmpl = serializer.validated_data['TENCENT_SMS_SIGN_AND_TEMPLATES']
|
||||
tencent_sdkappid = serializer.validated_data.get('TENCENT_SDKAPPID')
|
||||
|
||||
test_phone = serializer.validated_data.get('SMS_TEST_PHONE')
|
||||
|
||||
if not test_phone:
|
||||
raise JMSException(code='test_phone_required', detail=_('test_phone is required'))
|
||||
|
||||
if not tencent_secret_key:
|
||||
secret = Setting.objects.filter(name='TENCENT_SECRET_KEY').first()
|
||||
if secret:
|
||||
tencent_secret_key = secret.cleaned_value
|
||||
|
||||
tencent_secret_key = tencent_secret_key or ''
|
||||
|
||||
try:
|
||||
client = TencentSMS(
|
||||
secret_id=tencent_secret_id,
|
||||
secret_key=tencent_secret_key,
|
||||
sdkappid=tencent_sdkappid
|
||||
)
|
||||
sign, tmpl = SMS_MESSAGE.VERIFICATION_CODE.get_sign_and_tmpl(tencent_sms_sign_and_tmpl)
|
||||
|
||||
client.send_sms(
|
||||
phone_numbers=[test_phone],
|
||||
sign_name=sign,
|
||||
template_code=tmpl,
|
||||
template_param=OrderedDict(code='test')
|
||||
)
|
||||
return Response(status=status.HTTP_200_OK, data={'msg': _('Test success')})
|
||||
except APIException as e:
|
||||
try:
|
||||
error = e.detail['errmsg']
|
||||
except:
|
||||
error = e.detail
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': error})
|
|
@ -7,3 +7,4 @@ from .feishu import *
|
|||
from .wecom import *
|
||||
from .sso import *
|
||||
from .base import *
|
||||
from .sms import *
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.message.backends.sms import BACKENDS
|
||||
|
||||
__all__ = ['AlibabaSMSSettingSerializer', 'TencentSMSSettingSerializer']
|
||||
|
||||
|
||||
class BaseSMSSettingSerializer(serializers.Serializer):
|
||||
AUTH_SMS = serializers.BooleanField(default=False, label=_('Enable SMS'))
|
||||
SMS_TEST_PHONE = serializers.CharField(max_length=256, required=False, label=_('Test phone'))
|
||||
|
||||
def to_representation(self, instance):
|
||||
data = super().to_representation(instance)
|
||||
data['SMS_BACKEND'] = self.fields['SMS_BACKEND'].default
|
||||
return data
|
||||
|
||||
|
||||
class AlibabaSMSSettingSerializer(BaseSMSSettingSerializer):
|
||||
SMS_BACKEND = serializers.ChoiceField(choices=BACKENDS.choices, default=BACKENDS.ALIBABA)
|
||||
ALIBABA_ACCESS_KEY_ID = serializers.CharField(max_length=256, required=True, label='AccessKeyId')
|
||||
ALIBABA_ACCESS_KEY_SECRET = serializers.CharField(
|
||||
max_length=256, required=False, label='AccessKeySecret', write_only=True)
|
||||
ALIBABA_SMS_SIGN_AND_TEMPLATES = serializers.DictField(
|
||||
label=_('Signatures and Templates'), required=True, help_text=_('''
|
||||
Filling in JSON Data:
|
||||
{
|
||||
"verification_code": {
|
||||
"sign_name": "<Your signature name>",
|
||||
"template_code": "<Your template code>"
|
||||
}
|
||||
}
|
||||
''')
|
||||
)
|
||||
|
||||
|
||||
class TencentSMSSettingSerializer(BaseSMSSettingSerializer):
|
||||
SMS_BACKEND = serializers.ChoiceField(choices=BACKENDS.choices, default=BACKENDS.TENCENT)
|
||||
TENCENT_SECRET_ID = serializers.CharField(max_length=256, required=True, label='Secret id')
|
||||
TENCENT_SECRET_KEY = serializers.CharField(max_length=256, required=False, label='Secret key', write_only=True)
|
||||
TENCENT_SDKAPPID = serializers.CharField(max_length=256, required=True, label='SDK app id')
|
||||
TENCENT_SMS_SIGN_AND_TEMPLATES = serializers.DictField(
|
||||
label=_('Signatures and Templates'), required=True, help_text=_('''
|
||||
Filling in JSON Data:
|
||||
{
|
||||
"verification_code": {
|
||||
"sign_name": "<Your signature name>",
|
||||
"template_code": "<Your template code>"
|
||||
}
|
||||
}
|
||||
'''))
|
|
@ -6,12 +6,14 @@ from .email import EmailSettingSerializer, EmailContentSettingSerializer
|
|||
from .auth import (
|
||||
LDAPSettingSerializer, OIDCSettingSerializer, KeycloakSettingSerializer,
|
||||
CASSettingSerializer, RadiusSettingSerializer, FeiShuSettingSerializer,
|
||||
WeComSettingSerializer, DingTalkSettingSerializer
|
||||
WeComSettingSerializer, DingTalkSettingSerializer, AlibabaSMSSettingSerializer,
|
||||
TencentSMSSettingSerializer,
|
||||
)
|
||||
from .terminal import TerminalSettingSerializer
|
||||
from .security import SecuritySettingSerializer
|
||||
from .cleaning import CleaningSerializer
|
||||
|
||||
|
||||
__all__ = [
|
||||
'SettingsSerializer',
|
||||
]
|
||||
|
@ -32,7 +34,9 @@ class SettingsSerializer(
|
|||
KeycloakSettingSerializer,
|
||||
CASSettingSerializer,
|
||||
RadiusSettingSerializer,
|
||||
CleaningSerializer
|
||||
CleaningSerializer,
|
||||
AlibabaSMSSettingSerializer,
|
||||
TencentSMSSettingSerializer,
|
||||
):
|
||||
# encrypt_fields 现在使用 write_only 来判断了
|
||||
pass
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class SMSBackendSerializer(serializers.Serializer):
|
||||
name = serializers.CharField(max_length=256, required=True, label=_('Name'))
|
||||
label = serializers.CharField(max_length=256, required=True, label=_('Label'))
|
|
@ -16,6 +16,9 @@ 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/backend/', api.SMSBackendAPI.as_view(), name='sms-backend'),
|
||||
|
||||
path('setting/', api.SettingsApi.as_view(), name='settings-setting'),
|
||||
path('public/', api.PublicSettingApi.as_view(), name='public-setting'),
|
||||
|
|
|
@ -81,7 +81,12 @@ class CommandAlertMessage(CommandAlertMixin, SystemMessage):
|
|||
return message
|
||||
|
||||
def get_common_msg(self):
|
||||
return self._get_message()
|
||||
msg = self._get_message()
|
||||
|
||||
return {
|
||||
'subject': msg[:80],
|
||||
'message': msg
|
||||
}
|
||||
|
||||
def get_email_msg(self):
|
||||
command = self.command
|
||||
|
@ -140,9 +145,6 @@ class CommandExecutionAlert(CommandAlertMixin, SystemMessage):
|
|||
return message
|
||||
|
||||
def get_common_msg(self):
|
||||
return self._get_message()
|
||||
|
||||
def get_email_msg(self):
|
||||
command = self.command
|
||||
|
||||
subject = _("Insecure Web Command Execution Alert: [%(name)s]") % {
|
||||
|
|
|
@ -4,14 +4,13 @@ import uuid
|
|||
from rest_framework import generics
|
||||
from common.permissions import IsOrgAdmin
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from django.conf import settings
|
||||
|
||||
from users.notifications import ResetPasswordMsg, ResetPasswordSuccessMsg, ResetSSHKeyMsg
|
||||
from common.permissions import (
|
||||
IsCurrentUserOrReadOnly
|
||||
)
|
||||
from .. import serializers
|
||||
from ..models import User
|
||||
from ..utils import send_reset_password_success_mail
|
||||
from .mixins import UserQuerysetMixin
|
||||
|
||||
__all__ = [
|
||||
|
@ -29,11 +28,10 @@ class UserResetPasswordApi(UserQuerysetMixin, generics.UpdateAPIView):
|
|||
def perform_update(self, serializer):
|
||||
# Note: we are not updating the user object here.
|
||||
# We just do the reset-password stuff.
|
||||
from ..utils import send_reset_password_mail
|
||||
user = self.get_object()
|
||||
user.password_raw = str(uuid.uuid4())
|
||||
user.save()
|
||||
send_reset_password_mail(user)
|
||||
ResetPasswordMsg(user).publish_async()
|
||||
|
||||
|
||||
class UserResetPKApi(UserQuerysetMixin, generics.UpdateAPIView):
|
||||
|
@ -41,11 +39,11 @@ class UserResetPKApi(UserQuerysetMixin, generics.UpdateAPIView):
|
|||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
from ..utils import send_reset_ssh_key_mail
|
||||
user = self.get_object()
|
||||
user.public_key = None
|
||||
user.save()
|
||||
send_reset_ssh_key_mail(user)
|
||||
|
||||
ResetSSHKeyMsg(user).publish_async()
|
||||
|
||||
|
||||
# 废弃
|
||||
|
@ -84,4 +82,4 @@ class UserPublicKeyApi(generics.RetrieveUpdateAPIView):
|
|||
|
||||
def perform_update(self, serializer):
|
||||
super().perform_update(serializer)
|
||||
send_reset_password_success_mail(self.request, self.get_object())
|
||||
ResetPasswordSuccessMsg(self.get_object(), self.request).publish_async()
|
||||
|
|
|
@ -8,6 +8,7 @@ from rest_framework.response import Response
|
|||
from rest_framework_bulk import BulkModelViewSet
|
||||
from django.db.models import Prefetch
|
||||
|
||||
from users.notifications import ResetMFAMsg
|
||||
from common.permissions import (
|
||||
IsOrgAdmin, IsOrgAdminOrAppUser,
|
||||
CanUpdateDeleteUser, IsSuperUser
|
||||
|
@ -16,7 +17,7 @@ from common.mixins import CommonApiMixin
|
|||
from common.utils import get_logger
|
||||
from orgs.utils import current_org
|
||||
from orgs.models import ROLE as ORG_ROLE, OrganizationMember
|
||||
from users.utils import send_reset_mfa_mail, LoginBlockUtil, MFABlockUtils
|
||||
from users.utils import LoginBlockUtil, MFABlockUtils
|
||||
from .. import serializers
|
||||
from ..serializers import UserSerializer, UserRetrieveSerializer, MiniUserSerializer, InviteSerializer
|
||||
from .mixins import UserQuerysetMixin
|
||||
|
@ -209,5 +210,6 @@ class UserResetOTPApi(UserQuerysetMixin, generics.RetrieveAPIView):
|
|||
if user.mfa_enabled:
|
||||
user.reset_mfa()
|
||||
user.save()
|
||||
send_reset_mfa_mail(user)
|
||||
|
||||
ResetMFAMsg(user).publish_async()
|
||||
return Response({"msg": "success"})
|
||||
|
|
|
@ -8,3 +8,13 @@ class MFANotEnabled(JMSException):
|
|||
status_code = status.HTTP_403_FORBIDDEN
|
||||
default_code = 'mfa_not_enabled'
|
||||
default_detail = _('MFA not enabled')
|
||||
|
||||
|
||||
class PhoneNotSet(JMSException):
|
||||
default_code = 'phone_not_set'
|
||||
default_detail = _('Phone not set')
|
||||
|
||||
|
||||
class MFAMethodNotSupport(JMSException):
|
||||
default_code = 'mfa_not_support'
|
||||
default_detail = _('MFA method not support')
|
||||
|
|
|
@ -20,18 +20,24 @@ from django.shortcuts import reverse
|
|||
|
||||
from orgs.utils import current_org
|
||||
from orgs.models import OrganizationMember, Organization
|
||||
from common.exceptions import JMSException
|
||||
from common.utils import date_expired_default, get_logger, lazyproperty, random_string
|
||||
from common import fields
|
||||
from common.const import choices
|
||||
from common.db.models import TextChoices
|
||||
from users.exceptions import MFANotEnabled
|
||||
from users.exceptions import MFANotEnabled, PhoneNotSet
|
||||
from ..signals import post_user_change_password
|
||||
|
||||
__all__ = ['User', 'UserPasswordHistory']
|
||||
__all__ = ['User', 'UserPasswordHistory', 'MFAType']
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class MFAType(TextChoices):
|
||||
OTP = 'otp', _('One-time password')
|
||||
SMS_CODE = 'sms', _('SMS verify code')
|
||||
|
||||
|
||||
class AuthMixin:
|
||||
date_password_last_updated: datetime.datetime
|
||||
is_local: bool
|
||||
|
@ -514,19 +520,52 @@ class MFAMixin:
|
|||
from ..utils import check_otp_code
|
||||
return check_otp_code(self.otp_secret_key, code)
|
||||
|
||||
def check_mfa(self, code):
|
||||
def check_mfa(self, code, mfa_type=MFAType.OTP):
|
||||
if not self.mfa_enabled:
|
||||
raise MFANotEnabled
|
||||
|
||||
if settings.OTP_IN_RADIUS:
|
||||
return self.check_radius(code)
|
||||
else:
|
||||
return self.check_otp(code)
|
||||
if mfa_type == MFAType.OTP:
|
||||
if settings.OTP_IN_RADIUS:
|
||||
return self.check_radius(code)
|
||||
else:
|
||||
return self.check_otp(code)
|
||||
elif mfa_type == MFAType.SMS_CODE:
|
||||
return self.check_sms_code(code)
|
||||
|
||||
def get_supported_mfa_types(self):
|
||||
methods = []
|
||||
if self.otp_secret_key:
|
||||
methods.append(MFAType.OTP)
|
||||
if self.phone:
|
||||
methods.append(MFAType.SMS_CODE)
|
||||
return methods
|
||||
|
||||
def check_sms_code(self, code):
|
||||
from authentication.sms_verify_code import VerifyCodeUtil
|
||||
|
||||
if not self.phone:
|
||||
raise PhoneNotSet
|
||||
|
||||
try:
|
||||
util = VerifyCodeUtil(self.phone)
|
||||
return util.verify(code)
|
||||
except JMSException:
|
||||
return False
|
||||
|
||||
def send_sms_code(self):
|
||||
from authentication.sms_verify_code import VerifyCodeUtil
|
||||
|
||||
if not self.phone:
|
||||
raise PhoneNotSet
|
||||
|
||||
util = VerifyCodeUtil(self.phone)
|
||||
util.touch()
|
||||
return util.timeout
|
||||
|
||||
def mfa_enabled_but_not_set(self):
|
||||
if not self.mfa_enabled:
|
||||
return False, None
|
||||
if self.mfa_is_otp() and not self.otp_secret_key:
|
||||
if self.mfa_is_otp() and not self.otp_secret_key and not self.phone:
|
||||
return True, reverse('authentication:user-otp-enable-start')
|
||||
return False, None
|
||||
|
||||
|
|
|
@ -0,0 +1,389 @@
|
|||
from datetime import datetime
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from common.utils import reverse, get_request_ip_or_data, get_request_user_agent, lazyproperty
|
||||
from notifications.notifications import UserMessage
|
||||
|
||||
|
||||
class BaseUserMessage(UserMessage):
|
||||
def get_text_msg(self) -> dict:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
raise NotImplementedError
|
||||
|
||||
@lazyproperty
|
||||
def text_msg(self) -> dict:
|
||||
return self.get_text_msg()
|
||||
|
||||
@lazyproperty
|
||||
def html_msg(self) -> dict:
|
||||
return self.get_html_msg()
|
||||
|
||||
def get_dingtalk_msg(self) -> dict:
|
||||
return self.text_msg
|
||||
|
||||
def get_wecom_msg(self) -> dict:
|
||||
return self.text_msg
|
||||
|
||||
def get_feishu_msg(self) -> dict:
|
||||
return self.text_msg
|
||||
|
||||
def get_email_msg(self) -> dict:
|
||||
return self.html_msg
|
||||
|
||||
def get_site_msg_msg(self) -> dict:
|
||||
return self.html_msg
|
||||
|
||||
|
||||
class ResetPasswordMsg(BaseUserMessage):
|
||||
def get_text_msg(self) -> dict:
|
||||
user = self.user
|
||||
subject = _('Reset password')
|
||||
message = _("""
|
||||
Hello %(name)s:
|
||||
Please click the link below to reset your password, if not your request, concern your account security
|
||||
|
||||
Click here reset password 👇
|
||||
%(rest_password_url)s?token=%(rest_password_token)s
|
||||
|
||||
This link is valid for 1 hour. After it expires,
|
||||
|
||||
request new one 👇
|
||||
%(forget_password_url)s?email=%(email)s
|
||||
|
||||
-------------------
|
||||
|
||||
Login direct 👇
|
||||
%(login_url)s
|
||||
|
||||
""") % {
|
||||
'name': user.name,
|
||||
'rest_password_url': reverse('authentication:reset-password', external=True),
|
||||
'rest_password_token': user.generate_reset_token(),
|
||||
'forget_password_url': reverse('authentication:forgot-password', external=True),
|
||||
'email': user.email,
|
||||
'login_url': reverse('authentication:login', external=True),
|
||||
}
|
||||
return {
|
||||
'subject': subject,
|
||||
'message': message
|
||||
}
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
user = self.user
|
||||
subject = _('Reset password')
|
||||
message = _("""
|
||||
Hello %(name)s:
|
||||
<br>
|
||||
Please click the link below to reset your password, if not your request, concern your account security
|
||||
<br>
|
||||
<a href="%(rest_password_url)s?token=%(rest_password_token)s">Click here reset password</a>
|
||||
<br>
|
||||
This link is valid for 1 hour. After it expires, <a href="%(forget_password_url)s?email=%(email)s">request new one</a>
|
||||
|
||||
<br>
|
||||
---
|
||||
|
||||
<br>
|
||||
<a href="%(login_url)s">Login direct</a>
|
||||
|
||||
<br>
|
||||
""") % {
|
||||
'name': user.name,
|
||||
'rest_password_url': reverse('authentication:reset-password', external=True),
|
||||
'rest_password_token': user.generate_reset_token(),
|
||||
'forget_password_url': reverse('authentication:forgot-password', external=True),
|
||||
'email': user.email,
|
||||
'login_url': reverse('authentication:login', external=True),
|
||||
}
|
||||
return {
|
||||
'subject': subject,
|
||||
'message': message
|
||||
}
|
||||
|
||||
|
||||
class ResetPasswordSuccessMsg(BaseUserMessage):
|
||||
def __init__(self, user, request):
|
||||
super().__init__(user)
|
||||
self.ip_address = get_request_ip_or_data(request)
|
||||
self.browser = get_request_user_agent(request)
|
||||
|
||||
def get_text_msg(self) -> dict:
|
||||
user = self.user
|
||||
|
||||
subject = _('Reset password success')
|
||||
message = _("""
|
||||
|
||||
Hi %(name)s:
|
||||
|
||||
Your JumpServer password has just been successfully updated.
|
||||
|
||||
If the password update was not initiated by you, your account may have security issues.
|
||||
It is recommended that you log on to the JumpServer immediately and change your password.
|
||||
|
||||
If you have any questions, you can contact the administrator.
|
||||
|
||||
-------------------
|
||||
|
||||
|
||||
IP Address: %(ip_address)s
|
||||
<br>
|
||||
<br>
|
||||
Browser: %(browser)s
|
||||
<br>
|
||||
|
||||
""") % {
|
||||
'name': user.name,
|
||||
'ip_address': self.ip_address,
|
||||
'browser': self.browser,
|
||||
}
|
||||
return {
|
||||
'subject': subject,
|
||||
'message': message
|
||||
}
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
user = self.user
|
||||
|
||||
subject = _('Reset password success')
|
||||
message = _("""
|
||||
|
||||
Hi %(name)s:
|
||||
<br>
|
||||
|
||||
|
||||
<br>
|
||||
Your JumpServer password has just been successfully updated.
|
||||
<br>
|
||||
|
||||
<br>
|
||||
If the password update was not initiated by you, your account may have security issues.
|
||||
It is recommended that you log on to the JumpServer immediately and change your password.
|
||||
<br>
|
||||
|
||||
<br>
|
||||
If you have any questions, you can contact the administrator.
|
||||
<br>
|
||||
<br>
|
||||
---
|
||||
<br>
|
||||
<br>
|
||||
IP Address: %(ip_address)s
|
||||
<br>
|
||||
<br>
|
||||
Browser: %(browser)s
|
||||
<br>
|
||||
|
||||
""") % {
|
||||
'name': user.name,
|
||||
'ip_address': self.ip_address,
|
||||
'browser': self.browser,
|
||||
}
|
||||
return {
|
||||
'subject': subject,
|
||||
'message': message
|
||||
}
|
||||
|
||||
|
||||
class PasswordExpirationReminderMsg(BaseUserMessage):
|
||||
def get_text_msg(self) -> dict:
|
||||
user = self.user
|
||||
|
||||
subject = _('Security notice')
|
||||
message = _("""
|
||||
Hello %(name)s:
|
||||
|
||||
Your password will expire in %(date_password_expired)s,
|
||||
|
||||
For your account security, please click on the link below to update your password in time
|
||||
|
||||
Click here update password 👇
|
||||
%(update_password_url)s
|
||||
|
||||
If your password has expired, please click 👇 to apply for a password reset email.
|
||||
%(forget_password_url)s?email=%(email)s
|
||||
|
||||
-------------------
|
||||
|
||||
Login direct 👇
|
||||
%(login_url)s
|
||||
|
||||
""") % {
|
||||
'name': user.name,
|
||||
'date_password_expired': datetime.fromtimestamp(datetime.timestamp(
|
||||
user.date_password_expired)).strftime('%Y-%m-%d %H:%M'),
|
||||
'update_password_url': reverse('users:user-password-update', external=True),
|
||||
'forget_password_url': reverse('authentication:forgot-password', external=True),
|
||||
'email': user.email,
|
||||
'login_url': reverse('authentication:login', external=True),
|
||||
}
|
||||
return {
|
||||
'subject': subject,
|
||||
'message': message
|
||||
}
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
user = self.user
|
||||
|
||||
subject = _('Security notice')
|
||||
message = _("""
|
||||
Hello %(name)s:
|
||||
<br>
|
||||
Your password will expire in %(date_password_expired)s,
|
||||
<br>
|
||||
For your account security, please click on the link below to update your password in time
|
||||
<br>
|
||||
<a href="%(update_password_url)s">Click here update password</a>
|
||||
<br>
|
||||
If your password has expired, please click
|
||||
<a href="%(forget_password_url)s?email=%(email)s">Password expired</a>
|
||||
to apply for a password reset email.
|
||||
|
||||
<br>
|
||||
---
|
||||
|
||||
<br>
|
||||
<a href="%(login_url)s">Login direct</a>
|
||||
|
||||
<br>
|
||||
""") % {
|
||||
'name': user.name,
|
||||
'date_password_expired': datetime.fromtimestamp(datetime.timestamp(
|
||||
user.date_password_expired)).strftime('%Y-%m-%d %H:%M'),
|
||||
'update_password_url': reverse('users:user-password-update', external=True),
|
||||
'forget_password_url': reverse('authentication:forgot-password', external=True),
|
||||
'email': user.email,
|
||||
'login_url': reverse('authentication:login', external=True),
|
||||
}
|
||||
return {
|
||||
'subject': subject,
|
||||
'message': message
|
||||
}
|
||||
|
||||
|
||||
class UserExpirationReminderMsg(BaseUserMessage):
|
||||
def get_text_msg(self) -> dict:
|
||||
subject = _('Expiration notice')
|
||||
message = _("""
|
||||
Hello %(name)s:
|
||||
|
||||
Your account will expire in %(date_expired)s,
|
||||
|
||||
In order not to affect your normal work, please contact the administrator for confirmation.
|
||||
|
||||
""") % {
|
||||
'name': self.user.name,
|
||||
'date_expired': datetime.fromtimestamp(datetime.timestamp(
|
||||
self.user.date_expired)).strftime('%Y-%m-%d %H:%M'),
|
||||
}
|
||||
return {
|
||||
'subject': subject,
|
||||
'message': message
|
||||
}
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
subject = _('Expiration notice')
|
||||
message = _("""
|
||||
Hello %(name)s:
|
||||
<br>
|
||||
Your account will expire in %(date_expired)s,
|
||||
<br>
|
||||
In order not to affect your normal work, please contact the administrator for confirmation.
|
||||
<br>
|
||||
""") % {
|
||||
'name': self.user.name,
|
||||
'date_expired': datetime.fromtimestamp(datetime.timestamp(
|
||||
self.user.date_expired)).strftime('%Y-%m-%d %H:%M'),
|
||||
}
|
||||
return {
|
||||
'subject': subject,
|
||||
'message': message
|
||||
}
|
||||
|
||||
|
||||
class ResetSSHKeyMsg(BaseUserMessage):
|
||||
def get_text_msg(self) -> dict:
|
||||
subject = _('SSH Key Reset')
|
||||
message = _("""
|
||||
Hello %(name)s:
|
||||
|
||||
Your ssh public key has been reset by site administrator.
|
||||
Please login and reset your ssh public key.
|
||||
|
||||
Login direct 👇
|
||||
%(login_url)s
|
||||
|
||||
""") % {
|
||||
'name': self.user.name,
|
||||
'login_url': reverse('authentication:login', external=True),
|
||||
}
|
||||
|
||||
return {
|
||||
'subject': subject,
|
||||
'message': message
|
||||
}
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
subject = _('SSH Key Reset')
|
||||
message = _("""
|
||||
Hello %(name)s:
|
||||
<br>
|
||||
Your ssh public key has been reset by site administrator.
|
||||
Please login and reset your ssh public key.
|
||||
<br>
|
||||
<a href="%(login_url)s">Login direct</a>
|
||||
|
||||
<br>
|
||||
""") % {
|
||||
'name': self.user.name,
|
||||
'login_url': reverse('authentication:login', external=True),
|
||||
}
|
||||
|
||||
return {
|
||||
'subject': subject,
|
||||
'message': message
|
||||
}
|
||||
|
||||
|
||||
class ResetMFAMsg(BaseUserMessage):
|
||||
def get_text_msg(self) -> dict:
|
||||
subject = _('MFA Reset')
|
||||
message = _("""
|
||||
Hello %(name)s:
|
||||
|
||||
Your MFA has been reset by site administrator.
|
||||
Please login and reset your MFA.
|
||||
|
||||
Login direct 👇
|
||||
%(login_url)s
|
||||
|
||||
""") % {
|
||||
'name': self.user.name,
|
||||
'login_url': reverse('authentication:login', external=True),
|
||||
}
|
||||
return {
|
||||
'subject': subject,
|
||||
'message': message
|
||||
}
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
subject = _('MFA Reset')
|
||||
message = _("""
|
||||
Hello %(name)s:
|
||||
<br>
|
||||
Your MFA has been reset by site administrator.
|
||||
Please login and reset your MFA.
|
||||
<br>
|
||||
<a href="%(login_url)s">Login direct</a>
|
||||
|
||||
<br>
|
||||
""") % {
|
||||
'name': self.user.name,
|
||||
'login_url': reverse('authentication:login', external=True),
|
||||
}
|
||||
return {
|
||||
'subject': subject,
|
||||
'message': message
|
||||
}
|
|
@ -5,15 +5,14 @@ import sys
|
|||
from celery import shared_task
|
||||
from django.conf import settings
|
||||
|
||||
from users.notifications import PasswordExpirationReminderMsg
|
||||
from ops.celery.utils import (
|
||||
create_or_update_celery_periodic_tasks, disable_celery_periodic_task
|
||||
)
|
||||
from ops.celery.decorator import after_app_ready_start
|
||||
from common.utils import get_logger
|
||||
from .models import User
|
||||
from .utils import (
|
||||
send_password_expiration_reminder_mail, send_user_expiration_reminder_mail
|
||||
)
|
||||
from users.notifications import UserExpirationReminderMsg
|
||||
from settings.utils import LDAPServerUtil, LDAPImportUtil
|
||||
|
||||
|
||||
|
@ -30,7 +29,8 @@ def check_password_expired():
|
|||
continue
|
||||
msg = "The user {} password expires in {} days"
|
||||
logger.info(msg.format(user, user.password_expired_remain_days))
|
||||
send_password_expiration_reminder_mail(user)
|
||||
|
||||
PasswordExpirationReminderMsg(user).publish_async()
|
||||
|
||||
|
||||
@shared_task
|
||||
|
@ -57,7 +57,8 @@ def check_user_expired():
|
|||
continue
|
||||
msg = "The user {} will expires in {} days"
|
||||
logger.info(msg.format(user, user.expired_remain_days))
|
||||
send_user_expiration_reminder_mail(user)
|
||||
|
||||
UserExpirationReminderMsg(user).publish_async()
|
||||
|
||||
|
||||
@shared_task
|
||||
|
|
|
@ -10,7 +10,6 @@ import time
|
|||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.core.cache import cache
|
||||
from datetime import datetime
|
||||
|
||||
from common.tasks import send_mail_async
|
||||
from common.utils import reverse, get_object_or_none, get_request_ip_or_data, get_request_user_agent
|
||||
|
@ -79,184 +78,6 @@ def send_user_created_mail(user):
|
|||
send_mail_async.delay(subject, message, recipient_list, html_message=message)
|
||||
|
||||
|
||||
def send_reset_password_mail(user):
|
||||
subject = _('Reset password')
|
||||
recipient_list = [user.email]
|
||||
message = _("""
|
||||
Hello %(name)s:
|
||||
<br>
|
||||
Please click the link below to reset your password, if not your request, concern your account security
|
||||
<br>
|
||||
<a href="%(rest_password_url)s?token=%(rest_password_token)s">Click here reset password</a>
|
||||
<br>
|
||||
This link is valid for 1 hour. After it expires, <a href="%(forget_password_url)s?email=%(email)s">request new one</a>
|
||||
|
||||
<br>
|
||||
---
|
||||
|
||||
<br>
|
||||
<a href="%(login_url)s">Login direct</a>
|
||||
|
||||
<br>
|
||||
""") % {
|
||||
'name': user.name,
|
||||
'rest_password_url': reverse('authentication:reset-password', external=True),
|
||||
'rest_password_token': user.generate_reset_token(),
|
||||
'forget_password_url': reverse('authentication:forgot-password', external=True),
|
||||
'email': user.email,
|
||||
'login_url': reverse('authentication:login', external=True),
|
||||
}
|
||||
if settings.DEBUG:
|
||||
logger.debug(message)
|
||||
|
||||
send_mail_async.delay(subject, message, recipient_list, html_message=message)
|
||||
|
||||
|
||||
def send_reset_password_success_mail(request, user):
|
||||
subject = _('Reset password success')
|
||||
recipient_list = [user.email]
|
||||
message = _("""
|
||||
|
||||
Hi %(name)s:
|
||||
<br>
|
||||
|
||||
|
||||
<br>
|
||||
Your JumpServer password has just been successfully updated.
|
||||
<br>
|
||||
|
||||
<br>
|
||||
If the password update was not initiated by you, your account may have security issues.
|
||||
It is recommended that you log on to the JumpServer immediately and change your password.
|
||||
<br>
|
||||
|
||||
<br>
|
||||
If you have any questions, you can contact the administrator.
|
||||
<br>
|
||||
<br>
|
||||
---
|
||||
<br>
|
||||
<br>
|
||||
IP Address: %(ip_address)s
|
||||
<br>
|
||||
<br>
|
||||
Browser: %(browser)s
|
||||
<br>
|
||||
|
||||
""") % {
|
||||
'name': user.name,
|
||||
'ip_address': get_request_ip_or_data(request),
|
||||
'browser': get_request_user_agent(request),
|
||||
}
|
||||
if settings.DEBUG:
|
||||
logger.debug(message)
|
||||
|
||||
send_mail_async.delay(subject, message, recipient_list, html_message=message)
|
||||
|
||||
|
||||
def send_password_expiration_reminder_mail(user):
|
||||
subject = _('Security notice')
|
||||
recipient_list = [user.email]
|
||||
message = _("""
|
||||
Hello %(name)s:
|
||||
<br>
|
||||
Your password will expire in %(date_password_expired)s,
|
||||
<br>
|
||||
For your account security, please click on the link below to update your password in time
|
||||
<br>
|
||||
<a href="%(update_password_url)s">Click here update password</a>
|
||||
<br>
|
||||
If your password has expired, please click
|
||||
<a href="%(forget_password_url)s?email=%(email)s">Password expired</a>
|
||||
to apply for a password reset email.
|
||||
|
||||
<br>
|
||||
---
|
||||
|
||||
<br>
|
||||
<a href="%(login_url)s">Login direct</a>
|
||||
|
||||
<br>
|
||||
""") % {
|
||||
'name': user.name,
|
||||
'date_password_expired': datetime.fromtimestamp(datetime.timestamp(
|
||||
user.date_password_expired)).strftime('%Y-%m-%d %H:%M'),
|
||||
'update_password_url': reverse('users:user-password-update', external=True),
|
||||
'forget_password_url': reverse('authentication:forgot-password', external=True),
|
||||
'email': user.email,
|
||||
'login_url': reverse('authentication:login', external=True),
|
||||
}
|
||||
if settings.DEBUG:
|
||||
logger.debug(message)
|
||||
|
||||
send_mail_async.delay(subject, message, recipient_list, html_message=message)
|
||||
|
||||
|
||||
def send_user_expiration_reminder_mail(user):
|
||||
subject = _('Expiration notice')
|
||||
recipient_list = [user.email]
|
||||
message = _("""
|
||||
Hello %(name)s:
|
||||
<br>
|
||||
Your account will expire in %(date_expired)s,
|
||||
<br>
|
||||
In order not to affect your normal work, please contact the administrator for confirmation.
|
||||
<br>
|
||||
""") % {
|
||||
'name': user.name,
|
||||
'date_expired': datetime.fromtimestamp(datetime.timestamp(
|
||||
user.date_expired)).strftime('%Y-%m-%d %H:%M'),
|
||||
}
|
||||
if settings.DEBUG:
|
||||
logger.debug(message)
|
||||
|
||||
send_mail_async.delay(subject, message, recipient_list, html_message=message)
|
||||
|
||||
|
||||
def send_reset_ssh_key_mail(user):
|
||||
subject = _('SSH Key Reset')
|
||||
recipient_list = [user.email]
|
||||
message = _("""
|
||||
Hello %(name)s:
|
||||
<br>
|
||||
Your ssh public key has been reset by site administrator.
|
||||
Please login and reset your ssh public key.
|
||||
<br>
|
||||
<a href="%(login_url)s">Login direct</a>
|
||||
|
||||
<br>
|
||||
""") % {
|
||||
'name': user.name,
|
||||
'login_url': reverse('authentication:login', external=True),
|
||||
}
|
||||
if settings.DEBUG:
|
||||
logger.debug(message)
|
||||
|
||||
send_mail_async.delay(subject, message, recipient_list, html_message=message)
|
||||
|
||||
|
||||
def send_reset_mfa_mail(user):
|
||||
subject = _('MFA Reset')
|
||||
recipient_list = [user.email]
|
||||
message = _("""
|
||||
Hello %(name)s:
|
||||
<br>
|
||||
Your MFA has been reset by site administrator.
|
||||
Please login and reset your MFA.
|
||||
<br>
|
||||
<a href="%(login_url)s">Login direct</a>
|
||||
|
||||
<br>
|
||||
""") % {
|
||||
'name': user.name,
|
||||
'login_url': reverse('authentication:login', external=True),
|
||||
}
|
||||
if settings.DEBUG:
|
||||
logger.debug(message)
|
||||
|
||||
send_mail_async.delay(subject, message, recipient_list, html_message=message)
|
||||
|
||||
|
||||
def get_user_or_pre_auth_user(request):
|
||||
user = request.user
|
||||
if user.is_authenticated:
|
||||
|
|
|
@ -9,13 +9,13 @@ from django.conf import settings
|
|||
from django.urls import reverse_lazy
|
||||
from django.views.generic import FormView
|
||||
|
||||
from users.notifications import ResetPasswordSuccessMsg, ResetPasswordMsg
|
||||
from common.utils import get_object_or_none, FlashMessageUtil
|
||||
from common.permissions import IsValidUser
|
||||
from common.mixins.views import PermissionsMixin
|
||||
from ...models import User
|
||||
from ...utils import (
|
||||
send_reset_password_mail, get_password_check_rules, check_password_rules,
|
||||
send_reset_password_success_mail
|
||||
get_password_check_rules, check_password_rules,
|
||||
)
|
||||
from ... import forms
|
||||
|
||||
|
@ -59,7 +59,8 @@ class UserForgotPasswordView(FormView):
|
|||
).format(user.get_source_display())
|
||||
form.add_error('email', error)
|
||||
return self.form_invalid(form)
|
||||
send_reset_password_mail(user)
|
||||
|
||||
ResetPasswordMsg(user).publish_async()
|
||||
url = self.get_redirect_message_url()
|
||||
return redirect(url)
|
||||
|
||||
|
@ -115,7 +116,8 @@ class UserResetPasswordView(FormView):
|
|||
|
||||
user.reset_password(password)
|
||||
User.expired_reset_password_token(token)
|
||||
send_reset_password_success_mail(self.request, user)
|
||||
|
||||
ResetPasswordSuccessMsg(user, self.request).publish_async()
|
||||
url = self.get_redirect_url()
|
||||
return redirect(url)
|
||||
|
||||
|
|
|
@ -77,7 +77,7 @@ aliyun-python-sdk-core-v3==2.9.1
|
|||
aliyun-python-sdk-ecs==4.10.1
|
||||
rest_condition==1.0.3
|
||||
python-ldap==3.3.1
|
||||
tencentcloud-sdk-python==3.0.40
|
||||
tencentcloud-sdk-python==3.0.477
|
||||
django-radius==1.4.0
|
||||
ipip-ipdb==1.2.1
|
||||
django-redis-sessions==0.6.1
|
||||
|
@ -118,3 +118,4 @@ google-cloud-compute==0.5.0
|
|||
PyMySQL==1.0.2
|
||||
cx-Oracle==8.2.1
|
||||
psycopg2-binary==2.9.1
|
||||
alibabacloud_dysmsapi20170525==2.0.2
|
||||
|
|
Loading…
Reference in New Issue