Skip to content

Commit aee3aed

Browse files
authored
Add 2fa information to workspace users admin (baserow#4184)
1 parent ec4bb41 commit aee3aed

File tree

12 files changed

+116
-20
lines changed

12 files changed

+116
-20
lines changed

backend/src/baserow/api/two_factor_auth/serializers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@
88

99
class TwoFactorAuthSerializer(serializers.ModelSerializer):
1010
type = serializers.SerializerMethodField(read_only=True)
11+
is_enabled = serializers.BooleanField(read_only=True)
1112

1213
def get_type(self, instance):
1314
return instance.get_type().type
1415

1516
class Meta:
1617
model = TwoFactorAuthProviderModel
17-
fields = ["type"]
18+
fields = ["type", "is_enabled"]
1819

1920

2021
class CreateTwoFactorAuthSerializer(serializers.ModelSerializer):

backend/src/baserow/api/workspaces/users/serializers.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from rest_framework import serializers
66

77
from baserow.api.mixins import UnknownFieldRaisesExceptionSerializerMixin
8+
from baserow.api.two_factor_auth.serializers import TwoFactorAuthSerializer
89
from baserow.api.user.registries import member_data_registry
910
from baserow.core.generative_ai.registries import generative_ai_model_type_registry
1011
from baserow.core.models import WorkspaceUser
@@ -19,6 +20,7 @@ class WorkspaceUserSerializer(serializers.ModelSerializer):
1920
source="user.profile.to_be_deleted",
2021
help_text="True if user account is pending deletion.",
2122
)
23+
two_factor_auth = serializers.SerializerMethodField()
2224

2325
class Meta:
2426
model = WorkspaceUser
@@ -31,6 +33,7 @@ class Meta:
3133
"created_on",
3234
"user_id",
3335
"to_be_deleted",
36+
"two_factor_auth",
3437
)
3538

3639
@extend_schema_field(OpenApiTypes.STR)
@@ -41,6 +44,14 @@ def get_name(self, object):
4144
def get_email(self, object):
4245
return object.user.email
4346

47+
def get_two_factor_auth(self, object):
48+
try:
49+
provider = object.user.two_factor_auth_providers.all()[0]
50+
except IndexError:
51+
provider = None
52+
53+
return TwoFactorAuthSerializer(provider).data
54+
4455

4556
def get_member_data_types_request_serializer():
4657
"""

backend/src/baserow/api/workspaces/users/views.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.db import transaction
2+
from django.db.models import Prefetch
23

34
from drf_spectacular.openapi import OpenApiParameter, OpenApiTypes
45
from drf_spectacular.utils import extend_schema
@@ -29,6 +30,7 @@
2930
ERROR_CANNOT_DELETE_YOURSELF_FROM_GROUP,
3031
ERROR_GROUP_USER_DOES_NOT_EXIST,
3132
)
33+
from baserow.core.db import specific_queryset
3234
from baserow.core.exceptions import (
3335
CannotDeleteYourselfFromWorkspace,
3436
UserInvalidWorkspacePermissionsError,
@@ -39,6 +41,7 @@
3941
from baserow.core.handler import CoreHandler
4042
from baserow.core.models import WorkspaceUser
4143
from baserow.core.operations import ListWorkspaceUsersWorkspaceOperationType
44+
from baserow.core.two_factor_auth.models import TwoFactorAuthProviderModel
4245

4346
from .generated_serializers import ListWorkspaceUsersWithMemberDataSerializer
4447
from .serializers import (
@@ -123,8 +126,17 @@ def get(self, request, workspace_id, query_params):
123126
context=workspace,
124127
)
125128

126-
qs = WorkspaceUser.objects.filter(workspace=workspace).select_related(
127-
"workspace", "user", "user__profile"
129+
qs = (
130+
WorkspaceUser.objects.filter(workspace=workspace)
131+
.select_related("workspace", "user", "user__profile")
132+
.prefetch_related(
133+
Prefetch(
134+
"user__two_factor_auth_providers",
135+
queryset=specific_queryset(
136+
TwoFactorAuthProviderModel.objects.all()
137+
),
138+
)
139+
)
128140
)
129141

130142
qs = self.apply_search(search, qs)

backend/src/baserow/core/two_factor_auth/registries.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,11 @@ class TOTPAuthProviderType(TwoFactorAuthProviderType):
9494
type = "totp"
9595
model_class = TOTPAuthProviderModel
9696
serializer_field_names = [
97-
"enabled",
9897
"provisioning_url",
9998
"provisioning_qr_code",
10099
"backup_codes",
101100
]
102101
serializer_field_overrides = {
103-
"enabled": serializers.BooleanField(),
104102
"provisioning_url": serializers.CharField(),
105103
"provisioning_qr_code": serializers.CharField(),
106104
"backup_codes": serializers.ListField(child=serializers.CharField()),

backend/tests/baserow/api/groups/test_workspace_user_views.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,39 @@ def test_list_workspace_users(api_client, data_fixture):
6868
assert "created_on" in response_json[1]
6969

7070

71+
@pytest.mark.django_db
72+
def test_list_workspace_users_2fa_enabled(api_client, data_fixture):
73+
user_1, token_1 = data_fixture.create_user_and_token(email="test1@test.nl")
74+
user_2, token_2 = data_fixture.create_user_and_token(email="test2@test.nl")
75+
user_3, token_3 = data_fixture.create_user_and_token(email="test3@test.nl")
76+
data_fixture.configure_base_totp(user_1)
77+
data_fixture.configure_totp(user_2)
78+
79+
workspace_1 = data_fixture.create_workspace()
80+
data_fixture.create_user_workspace(
81+
workspace=workspace_1, user=user_1, permissions="ADMIN"
82+
)
83+
data_fixture.create_user_workspace(
84+
workspace=workspace_1, user=user_2, permissions="MEMBER"
85+
)
86+
data_fixture.create_user_workspace(
87+
workspace=workspace_1, user=user_3, permissions="MEMBER"
88+
)
89+
90+
response = api_client.get(
91+
reverse("api:workspaces:users:list", kwargs={"workspace_id": workspace_1.id}),
92+
HTTP_AUTHORIZATION=f"JWT {token_1}",
93+
)
94+
response_json = response.json()
95+
assert response.status_code == HTTP_200_OK
96+
assert len(response_json) == 3
97+
assert response_json[0]["two_factor_auth"]["type"] == "totp"
98+
assert response_json[1]["two_factor_auth"]["type"] == "totp"
99+
assert response_json[0]["two_factor_auth"]["is_enabled"] is False
100+
assert response_json[1]["two_factor_auth"]["is_enabled"] is True
101+
assert response_json[2]["two_factor_auth"] == {}
102+
103+
71104
@pytest.mark.django_db
72105
def test_update_workspace_user(api_client, data_fixture):
73106
user_1, token_1 = data_fixture.create_user_and_token(email="test1@test.nl")

backend/tests/baserow/api/two_factor_auth/test_two_factor_views.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def test_configuration_2fa_view_totp_not_enabled(api_client, data_fixture):
5959
assert response.status_code == HTTP_200_OK
6060
assert response_json == {
6161
"backup_codes": [],
62-
"enabled": False,
62+
"is_enabled": False,
6363
"provisioning_qr_code": AnyStr(),
6464
"provisioning_url": AnyStr(),
6565
"type": "totp",
@@ -81,7 +81,7 @@ def test_configuration_2fa_view_totp_enabled(api_client, data_fixture):
8181
assert response.status_code == HTTP_200_OK
8282
assert response_json == {
8383
"backup_codes": [],
84-
"enabled": True,
84+
"is_enabled": True,
8585
"provisioning_qr_code": "",
8686
"provisioning_url": "",
8787
"type": "totp",
@@ -188,7 +188,7 @@ def test_configure_totp_2fa_view(api_client, data_fixture):
188188
assert response.status_code == HTTP_200_OK, response_json
189189
assert response_json == {
190190
"backup_codes": [],
191-
"enabled": False,
191+
"is_enabled": False,
192192
"provisioning_qr_code": AnyStr(),
193193
"provisioning_url": AnyStr(),
194194
"type": "totp",
@@ -214,7 +214,7 @@ def test_configure_totp_2fa_view(api_client, data_fixture):
214214
assert response.status_code == HTTP_200_OK, response_json
215215
assert response_json == {
216216
"backup_codes": AnyList(),
217-
"enabled": True,
217+
"is_enabled": True,
218218
"provisioning_qr_code": "",
219219
"provisioning_url": "",
220220
"type": "totp",
@@ -239,7 +239,7 @@ def test_configure_totp_2fa_view_confirmation_failed_invalidcode(
239239
assert response.status_code == HTTP_200_OK, response_json
240240
assert response_json == {
241241
"backup_codes": [],
242-
"enabled": False,
242+
"is_enabled": False,
243243
"provisioning_qr_code": AnyStr(),
244244
"provisioning_url": AnyStr(),
245245
"type": "totp",
@@ -295,7 +295,7 @@ def test_configure_totp_2fa_view_replaces_previous_configuration(
295295
assert response.status_code == HTTP_200_OK, response_json
296296
assert response_json == {
297297
"backup_codes": [],
298-
"enabled": False,
298+
"is_enabled": False,
299299
"provisioning_qr_code": AnyStr(),
300300
"provisioning_url": AnyStr(),
301301
"type": "totp",
@@ -316,7 +316,7 @@ def test_configure_totp_2fa_view_replaces_previous_configuration(
316316
assert response.status_code == HTTP_200_OK, response_json2
317317
assert response_json2 == {
318318
"backup_codes": [],
319-
"enabled": False,
319+
"is_enabled": False,
320320
"provisioning_qr_code": AnyStr(),
321321
"provisioning_url": AnyStr(),
322322
"type": "totp",

enterprise/web-frontend/modules/baserow_enterprise/membersPagePluginTypes.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ export class EnterpriseMembersPagePluginType extends MembersPagePluginType {
2020
context
2121
)
2222

23-
const roleColumnIndex = columns.findIndex(
24-
(column) => column.key === 'permissions'
23+
const insertBeforeColumnIndex = columns.findIndex(
24+
(column) => column.key === 'two_factor_auth'
2525
)
2626
const highestRoleColumn = new CrudTableColumn(
2727
'highest_role_uid',
@@ -46,8 +46,8 @@ export class EnterpriseMembersPagePluginType extends MembersPagePluginType {
4646
{},
4747
20
4848
)
49-
columns.splice(roleColumnIndex, 0, highestRoleColumn)
50-
columns.splice(roleColumnIndex, 0, teamsColumn)
49+
columns.splice(insertBeforeColumnIndex, 0, highestRoleColumn)
50+
columns.splice(insertBeforeColumnIndex, 0, teamsColumn)
5151

5252
return columns
5353
}

web-frontend/modules/core/components/auth/TOTPLogin.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,6 @@ export default {
133133
const values = reactive({
134134
values: {
135135
backupCode: '',
136-
errorTitle: null,
137-
errorDescription: null,
138136
},
139137
})
140138
@@ -154,6 +152,8 @@ export default {
154152
enterBackupCode: false,
155153
loadingVerifyCode: false,
156154
loadingVerifyBackupCode: false,
155+
errorTitle: null,
156+
errorDescription: null,
157157
}
158158
},
159159
watch: {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<template>
2+
<Badge
3+
:color="row[column.key].is_enabled ? 'green' : 'neutral'"
4+
:rounded="true"
5+
>
6+
<span v-if="row[column.key].is_enabled">
7+
{{ $t('twoFactorAuthField.enabled') }}
8+
</span>
9+
<span v-else>
10+
{{ $t('twoFactorAuthField.disabled') }}
11+
</span>
12+
</Badge>
13+
</template>
14+
15+
<script>
16+
export default {
17+
name: 'TwoFactorAuthField',
18+
props: {
19+
row: {
20+
required: true,
21+
type: Object,
22+
},
23+
column: {
24+
required: true,
25+
type: Object,
26+
},
27+
},
28+
}
29+
</script>

web-frontend/modules/core/components/settings/TwoFactorAuthSettings.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export default {
6666
const { data } = await TwoFactorAuthService(
6767
this.$client
6868
).getConfiguration()
69-
if (data.enabled) {
69+
if (data.is_enabled) {
7070
this.state = 'enabled'
7171
this.provider = data
7272
}

0 commit comments

Comments
 (0)