Bulk edit permissions

This commit is contained in:
Michael Shamoon 2022-12-08 02:03:50 -08:00
parent 211fbf0cf6
commit 6ece5240a5
12 changed files with 267 additions and 99 deletions

View File

@ -108,6 +108,7 @@ import localeSr from '@angular/common/locales/sr'
import localeSv from '@angular/common/locales/sv'
import localeTr from '@angular/common/locales/tr'
import localeZh from '@angular/common/locales/zh'
import { PermissionsDialogComponent } from './components/common/permissions-dialog/permissions-dialog.component'
registerLocaleData(localeBe)
registerLocaleData(localeCs)
@ -203,6 +204,7 @@ function initializeApp(settings: SettingsService) {
PermissionsGroupComponent,
IfOwnerDirective,
IfObjectPermissionsDirective,
PermissionsDialogComponent,
],
imports: [
BrowserModule,

View File

@ -1,6 +1,6 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { interval, Subject, switchMap, take } from 'rxjs'
import { interval, Subject, take } from 'rxjs'
@Component({
selector: 'app-confirm-dialog',

View File

@ -0,0 +1,29 @@
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="cancelClicked()">
</button>
</div>
<div class="modal-body">
<p class="mb-0" *ngIf="message" [innerHTML]="message | safeHtml"></p>
<form [formGroup]="form">
<div formGroupName="set_permissions">
<h6 i18n>View</h6>
<div formGroupName="view">
<app-permissions-user type="view" formControlName="users"></app-permissions-user>
<app-permissions-group type="view" formControlName="groups"></app-permissions-group>
</div>
<h6 i18n>Edit</h6>
<div formGroupName="change">
<app-permissions-user type="change" formControlName="users"></app-permissions-user>
<app-permissions-group type="change" formControlName="groups"></app-permissions-group>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-primary" (click)="cancelClicked()" i18n>Cancel</button>
<button type="button" class="btn btn-primary" (click)="confirmClicked.emit(permissions)" i18n>Confirm</button>
</div>

View File

@ -0,0 +1,46 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
import { FormControl, FormGroup } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { PaperlessGroup } from 'src/app/data/paperless-group'
import { PaperlessUser } from 'src/app/data/paperless-user'
@Component({
selector: 'app-permissions-dialog',
templateUrl: './permissions-dialog.component.html',
styleUrls: ['./permissions-dialog.component.scss'],
})
export class PermissionsDialogComponent implements OnInit {
constructor(public activeModal: NgbActiveModal) {}
@Output()
public confirmClicked = new EventEmitter()
@Input()
title = $localize`Set Permissions`
form = new FormGroup({
set_permissions: new FormGroup({
view: new FormGroup({
users: new FormControl([]),
groups: new FormControl([]),
}),
change: new FormGroup({
users: new FormControl([]),
groups: new FormControl([]),
}),
}),
})
get permissions() {
return this.form.value['set_permissions']
}
@Input()
message = $localize`Note that permissions set here will override any existing permissions`
ngOnInit(): void {}
cancelClicked() {
this.activeModal.close()
}
}

View File

@ -65,7 +65,13 @@
</div>
</div>
<div class="col-auto ms-auto mb-2 mb-xl-0 d-flex">
<div class="btn-group btn-group-sm me-2">
<div class="btn-toolbar me-2">
<button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="setPermissions()">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-fill-lock" />
</svg>&nbsp;<ng-container i18n>Permissions</ng-container>
</button>
<div ngbDropdown class="me-2 d-flex">
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>

View File

@ -26,6 +26,7 @@ import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
@Component({
selector: 'app-bulk-editor',
@ -397,4 +398,16 @@ export class BulkEditorComponent extends ComponentWithPermissions {
this.executeBulkOperation(modal, 'redo_ocr', {})
})
}
setPermissions() {
let modal = this.modalService.open(PermissionsDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.confirmClicked.subscribe((permissions) => {
modal.componentInstance.buttonsEnabled = false
this.executeBulkOperation(modal, 'set_permissions', {
permissions,
})
})
}
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 861 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -5,6 +5,7 @@ from documents.models import Correspondent
from documents.models import Document
from documents.models import DocumentType
from documents.models import StoragePath
from documents.permissions import set_permissions_for_object
from documents.tasks import bulk_update_documents
from documents.tasks import update_document_archive_file
@ -128,3 +129,15 @@ def redo_ocr(doc_ids):
)
return "OK"
def set_permissions(doc_ids, permissions):
qs = Document.objects.filter(id__in=doc_ids)
for doc in qs:
set_permissions_for_object(permissions, doc)
affected_docs = [doc.id for doc in qs]
bulk_update_documents.delay(document_ids=affected_docs)
return "OK"

View File

@ -1,3 +1,11 @@
from django.contrib.auth.models import Group
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from guardian.models import GroupObjectPermission
from guardian.shortcuts import assign_perm
from guardian.shortcuts import get_users_with_perms
from guardian.shortcuts import remove_perm
from rest_framework.permissions import BasePermission
from rest_framework.permissions import DjangoObjectPermissions
@ -31,3 +39,65 @@ class PaperlessObjectPermissions(DjangoObjectPermissions):
class PaperlessAdminPermissions(BasePermission):
def has_permission(self, request, view):
return request.user.has_perm("admin.view_logentry")
def get_groups_with_only_permission(obj, codename):
ctype = ContentType.objects.get_for_model(obj)
permission = Permission.objects.get(content_type=ctype, codename=codename)
group_object_perm_group_ids = (
GroupObjectPermission.objects.filter(
object_pk=obj.pk,
content_type=ctype,
)
.filter(permission=permission)
.values_list("group_id")
)
return Group.objects.filter(id__in=group_object_perm_group_ids).distinct()
def set_permissions_for_object(permissions, object):
print(permissions, object)
for action in permissions:
permission = f"{action}_{object.__class__.__name__.lower()}"
# users
users_to_add = User.objects.filter(id__in=permissions[action]["users"])
users_to_remove = get_users_with_perms(
object,
only_with_perms_in=[permission],
)
if len(users_to_add) > 0 and len(users_to_remove) > 0:
users_to_remove = users_to_remove.difference(users_to_add)
if len(users_to_remove) > 0:
for user in users_to_remove:
remove_perm(permission, user, object)
if len(users_to_add) > 0:
for user in users_to_add:
assign_perm(permission, user, object)
if action == "change":
# change gives view too
assign_perm(
f"view_{object.__class__.__name__.lower()}",
user,
object,
)
# groups
groups_to_add = Group.objects.filter(id__in=permissions[action]["groups"])
groups_to_remove = get_groups_with_only_permission(
object,
permission,
)
if len(groups_to_add) > 0 and len(groups_to_remove) > 0:
groups_to_remove = groups_to_remove.difference(groups_to_add)
if len(groups_to_remove) > 0:
for group in groups_to_remove:
remove_perm(permission, group, object)
if len(groups_to_add) > 0:
for group in groups_to_add:
assign_perm(permission, group, object)
if action == "change":
# change gives view too
assign_perm(
f"view_{object.__class__.__name__.lower()}",
group,
object,
)

View File

@ -28,16 +28,13 @@ from .models import UiSettings
from .models import PaperlessTask
from .parsers import is_mime_type_supported
from guardian.models import GroupObjectPermission
from guardian.shortcuts import assign_perm
from guardian.shortcuts import remove_perm
from guardian.shortcuts import get_users_with_perms
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User
from django.contrib.auth.models import Group
from django.contrib.auth.models import Permission
from documents.permissions import get_groups_with_only_permission
from documents.permissions import set_permissions_for_object
# https://www.django-rest-framework.org/api-guide/serializers/#example
@ -85,56 +82,7 @@ class MatchingModelSerializer(serializers.ModelSerializer):
return match
def get_groups_with_only_permission(obj, codename):
ctype = ContentType.objects.get_for_model(obj)
permission = Permission.objects.get(content_type=ctype, codename=codename)
group_object_perm_group_ids = (
GroupObjectPermission.objects.filter(
object_pk=obj.pk,
content_type=ctype,
)
.filter(permission=permission)
.values_list("group_id")
)
return Group.objects.filter(id__in=group_object_perm_group_ids).distinct()
class OwnedObjectSerializer(serializers.ModelSerializer):
def get_permissions(self, obj):
view_codename = f"view_{obj.__class__.__name__.lower()}"
change_codename = f"change_{obj.__class__.__name__.lower()}"
return {
"view": {
"users": get_users_with_perms(
obj,
only_with_perms_in=[view_codename],
).values_list("id", flat=True),
"groups": get_groups_with_only_permission(
obj,
codename=view_codename,
).values_list("id", flat=True),
},
"change": {
"users": get_users_with_perms(
obj,
only_with_perms_in=[change_codename],
).values_list("id", flat=True),
"groups": get_groups_with_only_permission(
obj,
codename=change_codename,
).values_list("id", flat=True),
},
}
permissions = SerializerMethodField(read_only=True)
set_permissions = serializers.DictField(
label="Set permissions",
allow_empty=True,
required=False,
write_only=True,
)
class SetPermissionsMixin:
def _validate_user_ids(self, user_ids):
users = User.objects.none()
if user_ids is not None:
@ -169,52 +117,55 @@ class OwnedObjectSerializer(serializers.ModelSerializer):
if set_permissions is not None:
for action in permissions_dict:
users = set_permissions[action]["users"]
permissions_dict[action]["users"] = self._validate_user_ids(users)
self._validate_user_ids(users)
groups = set_permissions[action]["groups"]
permissions_dict[action]["groups"] = self._validate_group_ids(groups)
self._validate_group_ids(groups)
return permissions_dict
def _set_permissions(self, permissions, object):
set_permissions_for_object(permissions, object)
class OwnedObjectSerializer(serializers.ModelSerializer, SetPermissionsMixin):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user", None)
return super().__init__(*args, **kwargs)
def _set_permissions(self, permissions, object):
for action in permissions:
permission = f"{action}_{object.__class__.__name__.lower()}"
# users
users_to_add = permissions[action]["users"]
users_to_remove = get_users_with_perms(
object,
only_with_perms_in=[permission],
).difference(users_to_add)
for user in users_to_remove:
remove_perm(permission, user, object)
for user in users_to_add:
assign_perm(permission, user, object)
if action == "change":
# change gives view too
assign_perm(
f"view_{object.__class__.__name__.lower()}",
user,
object,
)
# groups
groups_to_add = permissions[action]["groups"]
groups_to_remove = get_groups_with_only_permission(
object,
permission,
).difference(groups_to_add)
for group in groups_to_remove:
remove_perm(permission, group, object)
for group in groups_to_add:
assign_perm(permission, group, object)
if action == "change":
# change gives view too
assign_perm(
f"view_{object.__class__.__name__.lower()}",
group,
object,
)
def get_permissions(self, obj):
view_codename = f"view_{obj.__class__.__name__.lower()}"
change_codename = f"change_{obj.__class__.__name__.lower()}"
return {
"view": {
"users": get_users_with_perms(
obj,
only_with_perms_in=[view_codename],
).values_list("id", flat=True),
"groups": get_groups_with_only_permission(
obj,
codename=view_codename,
).values_list("id", flat=True),
},
"change": {
"users": get_users_with_perms(
obj,
only_with_perms_in=[change_codename],
).values_list("id", flat=True),
"groups": get_groups_with_only_permission(
obj,
codename=change_codename,
).values_list("id", flat=True),
},
}
permissions = SerializerMethodField(read_only=True)
set_permissions = serializers.DictField(
label="Set permissions",
allow_empty=True,
required=False,
write_only=True,
)
# other methods in mixin
def create(self, validated_data):
if self.user and (
@ -515,7 +466,7 @@ class DocumentListSerializer(serializers.Serializer):
return documents
class BulkEditSerializer(DocumentListSerializer):
class BulkEditSerializer(DocumentListSerializer, SetPermissionsMixin):
method = serializers.ChoiceField(
choices=[
@ -527,6 +478,7 @@ class BulkEditSerializer(DocumentListSerializer):
"modify_tags",
"delete",
"redo_ocr",
"set_permissions",
],
label="Method",
write_only=True,
@ -562,6 +514,8 @@ class BulkEditSerializer(DocumentListSerializer):
return bulk_edit.delete
elif method == "redo_ocr":
return bulk_edit.redo_ocr
elif method == "set_permissions":
return bulk_edit.set_permissions
else:
raise serializers.ValidationError("Unsupported method.")
@ -625,6 +579,12 @@ class BulkEditSerializer(DocumentListSerializer):
else:
raise serializers.ValidationError("remove_tags not specified")
def _validate_parameters_set_permissions(self, parameters):
if "permissions" in parameters:
self.validate_set_permissions(parameters["permissions"])
else:
raise serializers.ValidationError("permissions not specified")
def validate(self, attrs):
method = attrs["method"]
@ -640,6 +600,8 @@ class BulkEditSerializer(DocumentListSerializer):
self._validate_parameters_modify_tags(parameters)
elif method == bulk_edit.set_storage_path:
self._validate_storage_path(parameters)
elif method == bulk_edit.set_permissions:
self._validate_parameters_set_permissions(parameters)
return attrs

View File

@ -41,6 +41,8 @@ from paperless import version
from rest_framework.test import APITestCase
from whoosh.writing import AsyncWriter
from guardian.shortcuts import get_users_with_perms
class TestDocumentApi(DirectoriesMixin, APITestCase):
def setUp(self):
@ -2329,6 +2331,31 @@ class TestBulkEdit(DirectoriesMixin, APITestCase):
],
)
def test_set_permissions(self):
user1 = User.objects.create(username="user1")
user2 = User.objects.create(username="user2")
permissions = {
"view": {
"users": User.objects.filter(id__in=[user1.id, user2.id]),
"groups": Group.objects.none(),
},
"change": {
"users": User.objects.filter(id__in=[user1.id]),
"groups": Group.objects.none(),
},
}
bulk_edit.set_permissions(
[self.doc2.id, self.doc3.id],
permissions=permissions,
)
self.assertEqual(get_users_with_perms(self.doc2).count(), 2)
self.async_task.assert_called_once()
args, kwargs = self.async_task.call_args
self.assertCountEqual(kwargs["document_ids"], [self.doc2.id, self.doc3.id])
class TestBulkDownload(DirectoriesMixin, APITestCase):
def setUp(self):