[Update] 修改mfa

pull/3454/head
ibuler 2019-11-18 16:30:26 +08:00
parent bfd8a9c66d
commit c9ee8edeaf
14 changed files with 92 additions and 56 deletions

View File

@ -31,11 +31,11 @@
<div class="form-group"> <div class="form-group">
<div class="col-sm-9 col-lg-9 col-sm-offset-2"> <div class="col-sm-9 col-lg-9 col-sm-offset-2">
<div class="checkbox checkbox-success"> <div class="checkbox checkbox-success">
<input type="checkbox" name="enable_otp" checked id="id_enable_otp"><label for="id_enable_otp">{% trans 'Enable-MFA' %}</label> <input type="checkbox" name="enable_mfa" checked id="id_enable_mfa"><label for="id_enable_mfa">{% trans 'Enable-MFA' %}</label>
</div> </div>
</div> </div>
</div> </div>
</form> </form>
{% endblock %} {% endblock %}
{% block modal_confirm_id %}btn_asset_group_bulk_update{% endblock %} {% block modal_confirm_id %}btn_asset_group_bulk_update{% endblock %}

View File

@ -128,7 +128,7 @@ def generate_data(username, request):
def on_user_auth_success(sender, user, request, **kwargs): def on_user_auth_success(sender, user, request, **kwargs):
logger.debug('User login success: {}'.format(user.username)) logger.debug('User login success: {}'.format(user.username))
data = generate_data(user.username, request) data = generate_data(user.username, request)
data.update({'mfa': int(user.otp_enabled), 'status': True}) data.update({'mfa': int(user.mfa_enabled), 'status': True})
write_login_log_async.delay(**data) write_login_log_async.delay(**data)

View File

@ -89,7 +89,9 @@ class AuthMixin:
def check_user_mfa_if_need(self, user): def check_user_mfa_if_need(self, user):
if self.request.session.get('auth_mfa'): if self.request.session.get('auth_mfa'):
return return
if not user.otp_enabled or not user.otp_secret_key: if not user.mfa_enabled:
return
if not user.otp_secret_key and user.mfa_is_otp():
return return
raise errors.MFARequiredError() raise errors.MFARequiredError()

View File

@ -130,8 +130,8 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView):
auth_login(self.request, user) auth_login(self.request, user)
self.send_auth_signal(success=True, user=user) self.send_auth_signal(success=True, user=user)
self.clear_auth_mark() self.clear_auth_mark()
# 启用但是没有设置otp # 启用但是没有设置otp, 排除radius
if user.otp_enabled and not user.otp_secret_key: if user.mfa_enabled_but_not_set():
# 1,2,mfa_setting & F # 1,2,mfa_setting & F
return reverse('users:user-otp-enable-authentication') return reverse('users:user-otp-enable-authentication')
url = redirect_user_first_login_or_index( url = redirect_user_first_login_or_index(

View File

@ -172,7 +172,7 @@ class UserResetOTPApi(UserQuerysetMixin, generics.RetrieveAPIView):
if user == request.user: if user == request.user:
msg = _("Could not reset self otp, use profile reset instead") msg = _("Could not reset self otp, use profile reset instead")
return Response({"error": msg}, status=401) return Response({"error": msg}, status=401)
if user.otp_enabled and user.otp_secret_key: if user.mfa_enabled and user.otp_secret_key:
user.otp_secret_key = '' user.otp_secret_key = ''
user.save() user.save()
logout(request) logout(request)

View File

@ -61,10 +61,10 @@ class UserCreateUpdateFormMixin(OrgModelForm):
fields = [ fields = [
'username', 'name', 'email', 'groups', 'wechat', 'username', 'name', 'email', 'groups', 'wechat',
'source', 'phone', 'role', 'date_expired', 'source', 'phone', 'role', 'date_expired',
'comment', 'otp_level' 'comment', 'mfa_level'
] ]
widgets = { widgets = {
'otp_level': forms.RadioSelect(), 'mfa_level': forms.RadioSelect(),
'groups': forms.SelectMultiple( 'groups': forms.SelectMultiple(
attrs={ attrs={
'class': 'select2', 'class': 'select2',
@ -126,13 +126,13 @@ class UserCreateUpdateFormMixin(OrgModelForm):
def save(self, commit=True): def save(self, commit=True):
password = self.cleaned_data.get('password') password = self.cleaned_data.get('password')
otp_level = self.cleaned_data.get('otp_level') mfa_level = self.cleaned_data.get('mfa_level')
public_key = self.cleaned_data.get('public_key') public_key = self.cleaned_data.get('public_key')
user = super().save(commit=commit) user = super().save(commit=commit)
if password: if password:
user.reset_password(password) user.reset_password(password)
if otp_level: if mfa_level:
user.otp_level = otp_level user.mfa_level = mfa_level
user.save() user.save()
if public_key: if public_key:
user.public_key = public_key user.public_key = public_key
@ -183,10 +183,10 @@ class UserMFAForm(forms.ModelForm):
class Meta: class Meta:
model = User model = User
fields = ['otp_level'] fields = ['mfa_level']
widgets = {'otp_level': forms.RadioSelect()} widgets = {'mfa_level': forms.RadioSelect()}
help_texts = { help_texts = {
'otp_level': _('* Enable MFA authentication ' 'mfa_level': _('* Enable MFA authentication '
'to make the account more secure.'), 'to make the account more secure.'),
} }

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.5 on 2019-11-18 08:12
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('users', '0023_auto_20190724_1525'),
]
operations = [
migrations.RenameField(
model_name='user',
old_name='otp_level',
new_name='mfa_level',
),
]

View File

@ -346,35 +346,41 @@ class TokenMixin:
class MFAMixin: class MFAMixin:
otp_level = 0 mfa_level = 0
otp_secret_key = '' otp_secret_key = ''
OTP_LEVEL_CHOICES = ( MFA_LEVEL_CHOICES = (
(0, _('Disable')), (0, _('Disable')),
(1, _('Enable')), (1, _('Enable')),
(2, _("Force enable")), (2, _("Force enable")),
) )
@property @property
def otp_enabled(self): def mfa_enabled(self):
return self.otp_force_enabled or self.otp_level > 0 return self.mfa_force_enabled or self.mfa_level > 0
@property @property
def otp_force_enabled(self): def mfa_force_enabled(self):
if settings.SECURITY_MFA_AUTH: if settings.SECURITY_MFA_AUTH:
return True return True
return self.otp_level == 2 return self.mfa_level == 2
def enable_otp(self): def enable_mfa(self):
if not self.otp_level == 2: if not self.mfa_level == 2:
self.otp_level = 1 self.mfa_level = 1
def force_enable_otp(self): def force_enable_mfa(self):
self.otp_level = 2 self.mfa_level = 2
def disable_otp(self): def disable_mfa(self):
self.otp_level = 0 self.mfa_level = 0
self.otp_secret_key = None self.otp_secret_key = None
@staticmethod
def mfa_is_otp():
if settings.CONFIG.OTP_IN_RADIUS:
return False
return True
def check_otp_on_radius(self, code): def check_otp_on_radius(self, code):
from authentication.backends.radius import RadiusBackend from authentication.backends.radius import RadiusBackend
backend = RadiusBackend() backend = RadiusBackend()
@ -390,6 +396,11 @@ class MFAMixin:
else: else:
return check_otp_code(self.otp_secret_key, code) return check_otp_code(self.otp_secret_key, code)
def mfa_enabled_but_not_set(self):
if self.mfa_enabled and self.mfa_is_otp() and not self.otp_secret_key:
return True
return False
class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser): class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
SOURCE_LOCAL = 'local' SOURCE_LOCAL = 'local'
@ -428,8 +439,8 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
phone = models.CharField( phone = models.CharField(
max_length=20, blank=True, null=True, verbose_name=_('Phone') max_length=20, blank=True, null=True, verbose_name=_('Phone')
) )
otp_level = models.SmallIntegerField( mfa_level = models.SmallIntegerField(
default=0, choices=MFAMixin.OTP_LEVEL_CHOICES, verbose_name=_('MFA') default=0, choices=MFAMixin.MFA_LEVEL_CHOICES, verbose_name=_('MFA')
) )
otp_secret_key = fields.EncryptCharField(max_length=128, blank=True, null=True) otp_secret_key = fields.EncryptCharField(max_length=128, blank=True, null=True)
# Todo: Auto generate key, let user download # Todo: Auto generate key, let user download

View File

@ -27,7 +27,7 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer):
fields = [ fields = [
'id', 'name', 'username', 'password', 'email', 'public_key', 'id', 'name', 'username', 'password', 'email', 'public_key',
'groups', 'groups_display', 'groups', 'groups_display',
'role', 'role_display', 'wechat', 'phone', 'otp_level', 'role', 'role_display', 'wechat', 'phone', 'mfa_level',
'comment', 'source', 'source_display', 'is_valid', 'is_expired', 'comment', 'source', 'source_display', 'is_valid', 'is_expired',
'is_active', 'created_by', 'is_first_login', 'is_active', 'created_by', 'is_first_login',
'date_password_last_updated', 'date_expired', 'avatar_url', 'date_password_last_updated', 'date_expired', 'avatar_url',

View File

@ -20,7 +20,7 @@
<h3>{% trans 'Auth' %}</h3> <h3>{% trans 'Auth' %}</h3>
{% block password %}{% endblock %} {% block password %}{% endblock %}
{% bootstrap_field form.otp_level layout="horizontal" %} {% bootstrap_field form.mfa_level layout="horizontal" %}
{% bootstrap_field form.source layout="horizontal" %} {% bootstrap_field form.source layout="horizontal" %}
<div class="hr-line-dashed"></div> <div class="hr-line-dashed"></div>
@ -77,7 +77,7 @@
$(document).ready(function () { $(document).ready(function () {
$('.select2').select2(); $('.select2').select2();
initDateRangePicker('#id_date_expired'); initDateRangePicker('#id_date_expired');
var mfa_radio = $('#id_otp_level'); var mfa_radio = $('#id_mfa_level');
mfa_radio.addClass("form-inline"); mfa_radio.addClass("form-inline");
mfa_radio.children().css("margin-right","15px"); mfa_radio.children().css("margin-right","15px");
fieldDisplay() fieldDisplay()

View File

@ -91,9 +91,9 @@
<tr> <tr>
<td>{% trans 'MFA certification' %}:</td> <td>{% trans 'MFA certification' %}:</td>
<td><b> <td><b>
{% if user_object.otp_force_enabled %} {% if user_object.mfa_force_enabled %}
{% trans 'Force enabled' %} {% trans 'Force enabled' %}
{% elif user_object.otp_enabled%} {% elif user_object.mfa_enabled%}
{% trans 'Enabled' %} {% trans 'Enabled' %}
{% else %} {% else %}
{% trans 'Disabled' %} {% trans 'Disabled' %}
@ -162,9 +162,9 @@
<td><span class="pull-right"> <td><span class="pull-right">
<div class="switch"> <div class="switch">
<div class="onoffswitch"> <div class="onoffswitch">
<input type="checkbox" class="onoffswitch-checkbox" {% if user_object.otp_force_enabled %} checked {% endif %} <input type="checkbox" class="onoffswitch-checkbox" {% if user_object.mfa_force_enabled %} checked {% endif %}
id="force_enable_otp"> id="force_enable_mfa">
<label class="onoffswitch-label" for="force_enable_otp"> <label class="onoffswitch-label" for="force_enable_mfa">
<span class="onoffswitch-inner"></span> <span class="onoffswitch-inner"></span>
<span class="onoffswitch-switch"></span> <span class="onoffswitch-switch"></span>
</label> </label>
@ -370,7 +370,7 @@ $(document).ready(function() {
success_message: success success_message: success
}); });
}) })
.on('click', '#force_enable_otp', function() { .on('click', '#force_enable_mfa', function() {
{% if request.user == user_object %} {% if request.user == user_object %}
toastr.error("{% trans 'Goto profile page enable MFA' %}"); toastr.error("{% trans 'Goto profile page enable MFA' %}");
return; return;
@ -378,16 +378,16 @@ $(document).ready(function() {
var the_url = "{% url 'api-users:user-detail' pk=user_object.id %}"; var the_url = "{% url 'api-users:user-detail' pk=user_object.id %}";
var checked = $(this).prop('checked'); var checked = $(this).prop('checked');
var otp_level; var mfa_level;
var otp_secret_key; var otp_secret_key;
if(checked){ if(checked){
otp_level = 2 mfa_level = 2
}else{ }else{
otp_level = 0; mfa_level = 0;
otp_secret_key = ''; otp_secret_key = '';
} }
var body = { var body = {
'otp_level': otp_level, 'mfa_level': mfa_level,
'otp_secret_key': otp_secret_key 'otp_secret_key': otp_secret_key
}; };
var success = '{% trans "Update successfully!" %}'; var success = '{% trans "Update successfully!" %}';

View File

@ -86,9 +86,9 @@
<tr> <tr>
<td class="text-navy">{% trans 'MFA certification' %}</td> <td class="text-navy">{% trans 'MFA certification' %}</td>
<td> <td>
{% if user.otp_force_enabled %} {% if user.mfa_force_enabled %}
{% trans 'Force enable' %} {% trans 'Force enable' %}
{% elif user.otp_enabled%} {% elif user.mfa_enabled%}
{% trans 'Enable' %} {% trans 'Enable' %}
{% else %} {% else %}
{% trans 'Disable' %} {% trans 'Disable' %}
@ -158,8 +158,8 @@
<span class="pull-right"> <span class="pull-right">
<a type="button" class="btn btn-primary btn-xs" style="width: 54px" id="" <a type="button" class="btn btn-primary btn-xs" style="width: 54px" id=""
href=" href="
{% if request.user.otp_enabled and request.user.otp_secret_key %} {% if request.user.mfa_enabled and request.user.otp_secret_key %}
{% if request.user.otp_force_enabled %} {% if request.user.mfa_force_enabled %}
" disabled >{% trans 'Disable' %} " disabled >{% trans 'Disable' %}
{% else %} {% else %}
{% url 'users:user-otp-disable-authentication' %} {% url 'users:user-otp-disable-authentication' %}
@ -183,7 +183,7 @@
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
{% if request.user.otp_enabled and request.user.otp_secret_key %} {% if request.user.mfa_enabled and request.user.otp_secret_key %}
<tr> <tr>
<td>{% trans 'Update MFA' %}:</td> <td>{% trans 'Update MFA' %}:</td>
<td> <td>

View File

@ -171,12 +171,12 @@ class UserFirstLoginView(PermissionsMixin, SessionWizardView):
form.instance = self.request.user form.instance = self.request.user
if isinstance(form, forms.UserMFAForm): if isinstance(form, forms.UserMFAForm):
choices = form.fields["otp_level"].choices choices = form.fields["mfa_level"].choices
if self.request.user.otp_force_enabled: if self.request.user.mfa_force_enabled:
choices = [(k, v) for k, v in choices if k == 2] choices = [(k, v) for k, v in choices if k == 2]
else: else:
choices = [(k, v) for k, v in choices if k in [0, 1]] choices = [(k, v) for k, v in choices if k in [0, 1]]
form.fields["otp_level"].choices = choices form.fields["mfa_level"].choices = choices
form.fields["otp_level"].initial = self.request.user.otp_level form.fields["mfa_level"].initial = self.request.user.mfa_level
return form return form

View File

@ -347,7 +347,12 @@ class UserOtpEnableAuthenticationView(FormView):
if not user: if not user:
form.add_error("password", _("Password invalid")) form.add_error("password", _("Password invalid"))
return self.form_invalid(form) return self.form_invalid(form)
return redirect(self.get_success_url()) if user.mfa_is_otp():
return redirect(self.get_success_url())
else:
user.enable_mfa()
user.save()
return redirect('users:user-otp-settings-success')
def get_success_url(self): def get_success_url(self):
return reverse('users:user-otp-enable-install-app') return reverse('users:user-otp-enable-install-app')
@ -395,7 +400,7 @@ class UserOtpEnableBindView(TemplateView, FormView):
def save_otp(self, otp_secret_key): def save_otp(self, otp_secret_key):
user = get_user_or_tmp_user(self.request) user = get_user_or_tmp_user(self.request)
user.enable_otp() user.enable_mfa()
user.otp_secret_key = otp_secret_key user.otp_secret_key = otp_secret_key
user.save() user.save()
@ -411,7 +416,7 @@ class UserOtpDisableAuthenticationView(FormView):
otp_secret_key = user.otp_secret_key otp_secret_key = user.otp_secret_key
if check_otp_code(otp_secret_key, otp_code): if check_otp_code(otp_secret_key, otp_code):
user.disable_otp() user.disable_mfa()
user.save() user.save()
return super().form_valid(form) return super().form_valid(form)
else: else:
@ -447,7 +452,7 @@ class UserOtpSettingsSuccessView(TemplateView):
auth_logout(self.request) auth_logout(self.request)
title = _('MFA enable success') title = _('MFA enable success')
describe = _('MFA enable success, return login page') describe = _('MFA enable success, return login page')
if not user.otp_enabled: if not user.mfa_enabled:
title = _('MFA disable success') title = _('MFA disable success')
describe = _('MFA disable success, return login page') describe = _('MFA disable success, return login page')