mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-05-03 11:29:28 -05:00
Allow superusers to disable 2fa
This commit is contained in:
parent
52ca8025d4
commit
70aa8d6cab
@ -32,6 +32,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<pngx-input-select i18n-title title="Groups" [items]="groups" multiple="true" formControlName="groups"></pngx-input-select>
|
<pngx-input-select i18n-title title="Groups" [items]="groups" multiple="true" formControlName="groups"></pngx-input-select>
|
||||||
|
|
||||||
|
@if (object?.is_mfa_enabled && currentUserIsSuperUser) {
|
||||||
|
<label class="form-label" i18n>Two-factor Authentication</label>
|
||||||
|
<pngx-confirm-button
|
||||||
|
label="Disable Two-factor Authentication"
|
||||||
|
i18n-label
|
||||||
|
title="Disable Two-factor Authentication"
|
||||||
|
i18n-title
|
||||||
|
buttonClasses="btn-outline-danger btn-sm"
|
||||||
|
iconName="trash"
|
||||||
|
[disabled]="totpLoading"
|
||||||
|
(confirm)="deactivateTotp()">
|
||||||
|
</pngx-confirm-button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<pngx-permissions-select i18n-title title="Permissions" formControlName="user_permissions" [error]="error?.user_permissions" [inheritedPermissions]="inheritedPermissions"></pngx-permissions-select>
|
<pngx-permissions-select i18n-title title="Permissions" formControlName="user_permissions" [error]="error?.user_permissions" [inheritedPermissions]="inheritedPermissions"></pngx-permissions-select>
|
||||||
|
@ -5,9 +5,11 @@ import { first } from 'rxjs'
|
|||||||
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||||
import { Group } from 'src/app/data/group'
|
import { Group } from 'src/app/data/group'
|
||||||
import { User } from 'src/app/data/user'
|
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 { GroupService } from 'src/app/services/rest/group.service'
|
||||||
import { UserService } from 'src/app/services/rest/user.service'
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-user-edit-dialog',
|
selector: 'pngx-user-edit-dialog',
|
||||||
@ -20,12 +22,15 @@ export class UserEditDialogComponent
|
|||||||
{
|
{
|
||||||
groups: Group[]
|
groups: Group[]
|
||||||
passwordIsSet: boolean = false
|
passwordIsSet: boolean = false
|
||||||
|
public totpLoading: boolean = false
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
service: UserService,
|
service: UserService,
|
||||||
activeModal: NgbActiveModal,
|
activeModal: NgbActiveModal,
|
||||||
groupsService: GroupService,
|
groupsService: GroupService,
|
||||||
settingsService: SettingsService
|
settingsService: SettingsService,
|
||||||
|
private toastService: ToastService,
|
||||||
|
private permissionsService: PermissionsService
|
||||||
) {
|
) {
|
||||||
super(service, activeModal, service, settingsService)
|
super(service, activeModal, service, settingsService)
|
||||||
|
|
||||||
@ -87,4 +92,30 @@ export class UserEditDialogComponent
|
|||||||
.length > 0
|
.length > 0
|
||||||
super.save()
|
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)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,4 +11,5 @@ export interface User extends ObjectWithId {
|
|||||||
groups?: number[] // Group[]
|
groups?: number[] // Group[]
|
||||||
user_permissions?: string[]
|
user_permissions?: string[]
|
||||||
inherited_permissions?: string[]
|
inherited_permissions?: string[]
|
||||||
|
is_mfa_enabled?: boolean
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,10 @@ export class PermissionsService {
|
|||||||
return this.currentUser?.is_staff
|
return this.currentUser?.is_staff
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isSuperUser(): boolean {
|
||||||
|
return this.currentUser?.is_superuser
|
||||||
|
}
|
||||||
|
|
||||||
public currentUserOwnsObject(object: ObjectWithPermissions): boolean {
|
public currentUserOwnsObject(object: ObjectWithPermissions): boolean {
|
||||||
return (
|
return (
|
||||||
!object ||
|
!object ||
|
||||||
|
@ -5,6 +5,7 @@ import { User } from 'src/app/data/user'
|
|||||||
import { PermissionsService } from '../permissions.service'
|
import { PermissionsService } from '../permissions.service'
|
||||||
import { AbstractNameFilterService } from './abstract-name-filter-service'
|
import { AbstractNameFilterService } from './abstract-name-filter-service'
|
||||||
|
|
||||||
|
const endpoint = 'users'
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
@ -13,7 +14,7 @@ export class UserService extends AbstractNameFilterService<User> {
|
|||||||
http: HttpClient,
|
http: HttpClient,
|
||||||
private permissionService: PermissionsService
|
private permissionService: PermissionsService
|
||||||
) {
|
) {
|
||||||
super(http, 'users')
|
super(http, endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
update(o: User): Observable<User> {
|
update(o: User): Observable<User> {
|
||||||
@ -31,4 +32,11 @@ export class UserService extends AbstractNameFilterService<User> {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deactivateTotp(u: User): Observable<boolean> {
|
||||||
|
return this.http.post<boolean>(
|
||||||
|
`${this.getResourceUrl(u.id, 'deactivate_totp')}`,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,11 @@ class UserSerializer(serializers.ModelSerializer):
|
|||||||
required=False,
|
required=False,
|
||||||
)
|
)
|
||||||
inherited_permissions = serializers.SerializerMethodField()
|
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:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
@ -50,6 +55,7 @@ class UserSerializer(serializers.ModelSerializer):
|
|||||||
"groups",
|
"groups",
|
||||||
"user_permissions",
|
"user_permissions",
|
||||||
"inherited_permissions",
|
"inherited_permissions",
|
||||||
|
"is_mfa_enabled",
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_inherited_permissions(self, obj):
|
def get_inherited_permissions(self, obj):
|
||||||
|
@ -17,6 +17,7 @@ from django.http import HttpResponseBadRequest
|
|||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
|
from rest_framework.decorators import action
|
||||||
from rest_framework.filters import OrderingFilter
|
from rest_framework.filters import OrderingFilter
|
||||||
from rest_framework.generics import GenericAPIView
|
from rest_framework.generics import GenericAPIView
|
||||||
from rest_framework.pagination import PageNumberPagination
|
from rest_framework.pagination import PageNumberPagination
|
||||||
@ -106,6 +107,24 @@ class UserViewSet(ModelViewSet):
|
|||||||
filterset_class = UserFilterSet
|
filterset_class = UserFilterSet
|
||||||
ordering_fields = ("username",)
|
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):
|
class GroupViewSet(ModelViewSet):
|
||||||
model = Group
|
model = Group
|
||||||
|
Loading…
x
Reference in New Issue
Block a user