Skip to content

Commit e0d72c4

Browse files
committed
Merge branch 'mfa-hotfixes' into release/2.022.24
2 parents 8760848 + e45fcca commit e0d72c4

22 files changed

+317
-60
lines changed

.github/workflows/pytest.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ jobs:
5656
run: >-
5757
sudo DEBIAN_FRONTEND=noninteractive apt-get -y install
5858
gdal-bin gettext libproj-dev postgresql-client ffmpeg
59+
gcc libc-dev
5960
- name: Install Python dependencies
6061
run: pip-sync dependencies/pip/dev_requirements.txt
6162
- name: Install JavaScript dependencies

jsapp/js/components/account/accountSidebar.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,19 @@ export default class AccountSidebar extends React.Component<
8383
</bem.FormSidebar__label>
8484
}
8585

86-
<bem.FormSidebar__label
87-
m={{selected: this.isSecuritySelected()}}
88-
href={'#' + ROUTES.SECURITY}
89-
disabled={ !(envStore.isReady && envStore.data.mfa_enabled) }
90-
>
91-
<Icon name='lock-alt' size='xl'/>
92-
<bem.FormSidebar__labelText>
93-
{t('Security')}
94-
</bem.FormSidebar__labelText>
95-
</bem.FormSidebar__label>
86+
{ /* hide "Security" entirely if nothing there is available */
87+
envStore.isReady && envStore.data.mfa_enabled &&
88+
<bem.FormSidebar__label
89+
m={{selected: this.isSecuritySelected()}}
90+
href={'#' + ROUTES.SECURITY}
91+
disabled={ !(envStore.isReady && envStore.data.mfa_enabled) }
92+
>
93+
<Icon name='lock-alt' size='xl'/>
94+
<bem.FormSidebar__labelText>
95+
{t('Security')}
96+
</bem.FormSidebar__labelText>
97+
</bem.FormSidebar__label>
98+
}
9699
</bem.FormSidebar>
97100
);
98101
}

kobo/apps/mfa/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,5 @@ class MfaAppConfig(AppConfig):
88

99
def ready(self):
1010
# Makes sure all signal handlers are connected
11-
# Uncomment the lines below if you need signals
12-
# from kobo.apps.mfa import signals
11+
from kobo.apps.mfa import signals
1312
super().ready()

kobo/apps/mfa/admin.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
from django.contrib import admin
33

44
from .models import (
5-
MFAMethod,
6-
KoboMFAMethod,
7-
KoboMFAMethodAdmin,
5+
TrenchMFAMethod,
6+
MfaAvailableToUser,
7+
MfaAvailableToUserAdmin,
8+
MfaMethod,
9+
MfaMethodAdmin,
810
)
911

10-
admin.site.unregister(MFAMethod)
11-
admin.site.register(KoboMFAMethod, KoboMFAMethodAdmin)
12+
admin.site.unregister(TrenchMFAMethod)
13+
admin.site.register(MfaMethod, MfaMethodAdmin)
14+
admin.site.register(MfaAvailableToUser, MfaAvailableToUserAdmin)

kobo/apps/mfa/forms.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
)
1111

1212

13-
class MFALoginForm(AuthenticationForm):
13+
class MfaLoginForm(AuthenticationForm):
1414
"""
1515
Authenticating users.
1616
If 2FA is activated, first step (of two) of the login process.
@@ -42,7 +42,7 @@ def get_ephemeral_token(self):
4242
return self.ephemeral_token_cache
4343

4444

45-
class MFATokenForm(forms.Form):
45+
class MfaTokenForm(forms.Form):
4646
"""
4747
Validate 2FA token.
4848
Second (and last) step of login process when MFA is activated.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Generated by Django 2.2.27 on 2022-07-05 18:51
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12+
('mfa', '0001_initial'),
13+
]
14+
15+
operations = [
16+
migrations.AlterModelOptions(
17+
name='kobomfamethod',
18+
options={'verbose_name': 'MFA Method', 'verbose_name_plural': 'MFA Methods'},
19+
),
20+
migrations.CreateModel(
21+
name='MfaAvailableToUser',
22+
fields=[
23+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
24+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
25+
],
26+
options={
27+
'verbose_name': 'per-user availability',
28+
'verbose_name_plural': 'per-user availabilities',
29+
},
30+
),
31+
]
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Generated by Django 2.2.27 on 2022-07-05 19:19
2+
3+
from django.conf import settings
4+
from django.db import migrations
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
11+
('trench', '0003_auto_20190213_2330'),
12+
('mfa', '0002_add_mfa_available_to_user_model'),
13+
]
14+
15+
operations = [
16+
migrations.RenameModel(
17+
old_name='KoboMFAMethod',
18+
new_name='MfaMethod',
19+
),
20+
]

kobo/apps/mfa/models.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,41 @@
11
# coding: utf-8
22
from django.conf import settings
3+
from django.contrib import admin
34
from django.db import models
45
from django.utils.timezone import now
5-
from trench.admin import MFAMethod, MFAMethodAdmin
6+
from trench.admin import (
7+
MFAMethod as TrenchMFAMethod,
8+
MFAMethodAdmin as TrenchMFAMethodAdmin,
9+
)
610

711
from kpi.deployment_backends.kc_access.shadow_models import (
812
KobocatUserProfile,
913
)
1014

1115

12-
class KoboMFAMethod(MFAMethod):
16+
class MfaAvailableToUser(models.Model):
17+
18+
class Meta:
19+
verbose_name = 'per-user availability'
20+
verbose_name_plural = 'per-user availabilities'
21+
user = models.ForeignKey('auth.User', on_delete=models.CASCADE)
22+
23+
def __str__(self):
24+
# Used to display the user-friendly representation of MfaAvailableToUser
25+
# objects, especially in Django Admin interface.
26+
return f'MFA available to user {self.user.username}'
27+
28+
29+
class MfaAvailableToUserAdmin(admin.ModelAdmin):
30+
31+
search_fields = ('user__username',)
32+
autocomplete_fields = ['user']
33+
# To customize list columns, uncomment line below to use instead of string
34+
# representation of `MfaAvailableToUser` objects
35+
# list_display = ('user',)
36+
37+
38+
class MfaMethod(TrenchMFAMethod):
1339
"""
1440
Extend DjangoTrench model to add created, modified and last disabled date
1541
"""
@@ -65,6 +91,6 @@ def delete(self, using=None, keep_parents=False):
6591
)
6692

6793

68-
class KoboMFAMethodAdmin(MFAMethodAdmin):
94+
class MfaMethodAdmin(TrenchMFAMethodAdmin):
6995

7096
search_fields = ['user__username']

kobo/apps/mfa/serializers.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,29 @@
11
# coding: utf-8
22
from rest_framework import serializers
3-
from trench.utils import get_mfa_model
3+
from trench.serializers import RequestMFAMethodActivationSerializer
4+
from trench.utils import create_secret, get_mfa_model
45

56

6-
class UserMFAMethodSerializer(serializers.ModelSerializer):
7+
class ActivateMfaMethodSerializer(RequestMFAMethodActivationSerializer):
8+
def create(self, validated_data):
9+
"""
10+
When the client requests creation of a new MFA method, *always* create
11+
a new secret. The default django-trench behavior reuses old secrets,
12+
meaning that without this override, a given user would always see the
13+
same QR code no matter how many times they enabled and disabled MFA.
14+
That default behavior would create a security weakness if a user lost
15+
their original device, because deactivating MFA and reconfiguring it on
16+
a new device would not prevent the lost device from continuing to
17+
generate valid TOTPs.
18+
"""
19+
mfa_method, created = super().create(validated_data)
20+
if not created:
21+
mfa_method.secret = create_secret()
22+
mfa_method.save()
23+
return mfa_method, created
24+
25+
26+
class UserMfaMethodSerializer(serializers.ModelSerializer):
727
"""
828
Exposes user's MFA methods and their created, modified and disabled dates
929
"""

kobo/apps/mfa/signals.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# coding: utf-8
2+
from django.db.models.signals import pre_delete
3+
from django.dispatch import receiver
4+
5+
from .models import MfaAvailableToUser, MfaMethod
6+
7+
8+
@receiver(pre_delete, sender=MfaAvailableToUser)
9+
def deactivate_mfa_method_for_user(**kwargs):
10+
"""
11+
Deactivate MFA methods for user on delete (and bulk delete)
12+
"""
13+
# We need to use a signal (instead of adding this code to
14+
# `MfaAvailableToUser.delete()`) because of bulk deletes which do not
15+
# call `.delete()`.
16+
17+
mfa_available_to_user = kwargs['instance']
18+
# Deactivate any MFA methods user could have already created
19+
try:
20+
# Use `.get()` + `.save()` (from model `MfaMethod`) instead of
21+
# `.update()` to run some logic inside `.save()`. It makes an extra
22+
# query to DB but avoid duplicated code.
23+
mfa_method = MfaMethod.objects.get(user=mfa_available_to_user.user)
24+
except MfaMethod.DoesNotExist:
25+
pass
26+
else:
27+
mfa_method.is_active = False
28+
mfa_method.save(update_fields=['is_active'])

0 commit comments

Comments
 (0)