From 70aa8d6cab5f17e9e56da59d32cb57bbeca40e7b Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sat, 19 Oct 2024 21:23:30 -0700 Subject: [PATCH] Allow superusers to disable 2fa --- .../user-edit-dialog.component.html | 14 ++++++++ .../user-edit-dialog.component.ts | 33 ++++++++++++++++++- src-ui/src/app/data/user.ts | 1 + .../src/app/services/permissions.service.ts | 4 +++ src-ui/src/app/services/rest/user.service.ts | 10 +++++- src/paperless/serialisers.py | 6 ++++ src/paperless/views.py | 19 +++++++++++ 7 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html b/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html index ca834a3ad..a2b3db67d 100644 --- a/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html +++ b/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html @@ -32,6 +32,20 @@ + + @if (object?.is_mfa_enabled && currentUserIsSuperUser) { + + + + }
diff --git a/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts index baadfa541..acd327d3a 100644 --- a/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts +++ b/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts @@ -5,9 +5,11 @@ import { first } from 'rxjs' import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' import { Group } from 'src/app/data/group' import { User } from 'src/app/data/user' +import { PermissionsService } from 'src/app/services/permissions.service' import { GroupService } from 'src/app/services/rest/group.service' import { UserService } from 'src/app/services/rest/user.service' import { SettingsService } from 'src/app/services/settings.service' +import { ToastService } from 'src/app/services/toast.service' @Component({ selector: 'pngx-user-edit-dialog', @@ -20,12 +22,15 @@ export class UserEditDialogComponent { groups: Group[] passwordIsSet: boolean = false + public totpLoading: boolean = false constructor( service: UserService, activeModal: NgbActiveModal, groupsService: GroupService, - settingsService: SettingsService + settingsService: SettingsService, + private toastService: ToastService, + private permissionsService: PermissionsService ) { super(service, activeModal, service, settingsService) @@ -87,4 +92,30 @@ export class UserEditDialogComponent .length > 0 super.save() } + + get currentUserIsSuperUser(): boolean { + return this.permissionsService.isSuperUser() + } + + deactivateTotp() { + this.totpLoading = true + ;(this.service as UserService) + .deactivateTotp(this.object) + .pipe(first()) + .subscribe({ + next: (result) => { + this.totpLoading = false + if (result) { + this.toastService.showInfo($localize`Totp deactivated`) + this.object.is_mfa_enabled = false + } else { + this.toastService.showError($localize`Totp deactivation failed`) + } + }, + error: (e) => { + this.totpLoading = false + this.toastService.showError($localize`Totp deactivation failed`, e) + }, + }) + } } diff --git a/src-ui/src/app/data/user.ts b/src-ui/src/app/data/user.ts index 49216a274..6bf051c25 100644 --- a/src-ui/src/app/data/user.ts +++ b/src-ui/src/app/data/user.ts @@ -11,4 +11,5 @@ export interface User extends ObjectWithId { groups?: number[] // Group[] user_permissions?: string[] inherited_permissions?: string[] + is_mfa_enabled?: boolean } diff --git a/src-ui/src/app/services/permissions.service.ts b/src-ui/src/app/services/permissions.service.ts index c80bc763d..3d88b10cc 100644 --- a/src-ui/src/app/services/permissions.service.ts +++ b/src-ui/src/app/services/permissions.service.ts @@ -56,6 +56,10 @@ export class PermissionsService { return this.currentUser?.is_staff } + public isSuperUser(): boolean { + return this.currentUser?.is_superuser + } + public currentUserOwnsObject(object: ObjectWithPermissions): boolean { return ( !object || diff --git a/src-ui/src/app/services/rest/user.service.ts b/src-ui/src/app/services/rest/user.service.ts index 4fb02b1f7..ded7ae248 100644 --- a/src-ui/src/app/services/rest/user.service.ts +++ b/src-ui/src/app/services/rest/user.service.ts @@ -5,6 +5,7 @@ import { User } from 'src/app/data/user' import { PermissionsService } from '../permissions.service' import { AbstractNameFilterService } from './abstract-name-filter-service' +const endpoint = 'users' @Injectable({ providedIn: 'root', }) @@ -13,7 +14,7 @@ export class UserService extends AbstractNameFilterService { http: HttpClient, private permissionService: PermissionsService ) { - super(http, 'users') + super(http, endpoint) } update(o: User): Observable { @@ -31,4 +32,11 @@ export class UserService extends AbstractNameFilterService { }) ) } + + deactivateTotp(u: User): Observable { + return this.http.post( + `${this.getResourceUrl(u.id, 'deactivate_totp')}`, + null + ) + } } diff --git a/src/paperless/serialisers.py b/src/paperless/serialisers.py index 5106bcf08..d5acfe465 100644 --- a/src/paperless/serialisers.py +++ b/src/paperless/serialisers.py @@ -33,6 +33,11 @@ class UserSerializer(serializers.ModelSerializer): required=False, ) inherited_permissions = serializers.SerializerMethodField() + is_mfa_enabled = serializers.SerializerMethodField() + + def get_is_mfa_enabled(self, user: User): + mfa_adapter = get_mfa_adapter() + return mfa_adapter.is_mfa_enabled(user) class Meta: model = User @@ -50,6 +55,7 @@ class UserSerializer(serializers.ModelSerializer): "groups", "user_permissions", "inherited_permissions", + "is_mfa_enabled", ) def get_inherited_permissions(self, obj): diff --git a/src/paperless/views.py b/src/paperless/views.py index 0157b885e..ea9d0b89e 100644 --- a/src/paperless/views.py +++ b/src/paperless/views.py @@ -17,6 +17,7 @@ from django.http import HttpResponseBadRequest from django.views.generic import View from django_filters.rest_framework import DjangoFilterBackend from rest_framework.authtoken.models import Token +from rest_framework.decorators import action from rest_framework.filters import OrderingFilter from rest_framework.generics import GenericAPIView from rest_framework.pagination import PageNumberPagination @@ -106,6 +107,24 @@ class UserViewSet(ModelViewSet): filterset_class = UserFilterSet ordering_fields = ("username",) + @action(detail=True, methods=["post"]) + def deactivate_totp(self, request, pk=None): + request_user = request.user + user = User.objects.get(pk=pk) + if not request_user.is_superuser and request_user != user: + return HttpResponseBadRequest( + "You do not have permission to deactivate TOTP for this user", + ) + try: + authenticator = Authenticator.objects.filter( + user=user, + type=Authenticator.Type.TOTP, + ).first() + delete_and_cleanup(request, authenticator) + return Response(True) + except Authenticator.DoesNotExist: + return HttpResponseBadRequest("TOTP not found") + class GroupViewSet(ModelViewSet): model = Group