Merge branch 'dev'

This commit is contained in:
shamoon
2024-02-25 18:37:19 -08:00
33 changed files with 1528 additions and 518 deletions

View File

@@ -286,10 +286,10 @@ class Command(BaseCommand):
def handle_inotify(self, directory, recursive, is_testing: bool):
logger.info(f"Using inotify to watch directory for changes: {directory}")
timeout = None
timeout_ms = None
if is_testing:
timeout = self.testing_timeout_ms
logger.debug(f"Configuring timeout to {timeout}ms")
timeout_ms = self.testing_timeout_ms
logger.debug(f"Configuring timeout to {timeout_ms}ms")
inotify = INotify()
inotify_flags = flags.CLOSE_WRITE | flags.MOVED_TO | flags.MODIFY
@@ -298,7 +298,8 @@ class Command(BaseCommand):
else:
descriptor = inotify.add_watch(directory, inotify_flags)
inotify_debounce: Final[float] = settings.CONSUMER_INOTIFY_DELAY
inotify_debounce_secs: Final[float] = settings.CONSUMER_INOTIFY_DELAY
inotify_debounce_ms: Final[int] = inotify_debounce_secs * 1000
finished = False
@@ -306,7 +307,7 @@ class Command(BaseCommand):
while not finished:
try:
for event in inotify.read(timeout=timeout):
for event in inotify.read(timeout=timeout_ms):
path = inotify.get_path(event.wd) if recursive else directory
filepath = os.path.join(path, event.name)
if flags.MODIFY in flags.from_mask(event.mask):
@@ -323,7 +324,7 @@ class Command(BaseCommand):
# Current time - last time over the configured timeout
waited_long_enough = (
monotonic() - last_event_time
) > inotify_debounce
) > inotify_debounce_secs
# Also make sure the file exists still, some scanners might write a
# temporary file first
@@ -342,11 +343,11 @@ class Command(BaseCommand):
# If files are waiting, need to exit read() to check them
# Otherwise, go back to infinite sleep time, but only if not testing
if len(notified_files) > 0:
timeout = inotify_debounce
timeout_ms = inotify_debounce_ms
elif is_testing:
timeout = self.testing_timeout_ms
timeout_ms = self.testing_timeout_ms
else:
timeout = None
timeout_ms = None
if self.stop_flag.is_set():
logger.debug("Finishing because event is set")

View File

@@ -4,26 +4,17 @@ import django.db.models.deletion
import multiselectfield.db.fields
from django.conf import settings
from django.contrib.auth.management import create_permissions
from django.contrib.auth.models import Group
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.db import migrations
from django.db import models
from django.db import transaction
from django.db.models import Q
from documents.models import Correspondent
from documents.models import CustomField
from documents.models import DocumentType
from documents.models import StoragePath
from documents.models import Tag
from documents.models import Workflow
from documents.models import WorkflowAction
from documents.models import WorkflowTrigger
from paperless_mail.models import MailRule
def add_workflow_permissions(apps, schema_editor):
app_name = "auth"
User = apps.get_model(app_label=app_name, model_name="User")
Group = apps.get_model(app_label=app_name, model_name="Group")
Permission = apps.get_model(app_label=app_name, model_name="Permission")
# create permissions without waiting for post_migrate signal
for app_config in apps.get_app_configs():
app_config.models_module = True
@@ -43,6 +34,10 @@ def add_workflow_permissions(apps, schema_editor):
def remove_workflow_permissions(apps, schema_editor):
app_name = "auth"
User = apps.get_model(app_label=app_name, model_name="User")
Group = apps.get_model(app_label=app_name, model_name="Group")
Permission = apps.get_model(app_label=app_name, model_name="Permission")
workflow_permissions = Permission.objects.filter(
codename__contains="workflow",
)
@@ -59,15 +54,28 @@ def migrate_consumption_templates(apps, schema_editor):
Migrate consumption templates to workflows. At this point ConsumptionTemplate still exists
but objects are not returned as their true model so we have to manually do that
"""
model_name = "ConsumptionTemplate"
app_name = "documents"
ConsumptionTemplate = apps.get_model(app_label=app_name, model_name=model_name)
ConsumptionTemplate = apps.get_model(
app_label=app_name,
model_name="ConsumptionTemplate",
)
Workflow = apps.get_model(app_label=app_name, model_name="Workflow")
WorkflowAction = apps.get_model(app_label=app_name, model_name="WorkflowAction")
WorkflowTrigger = apps.get_model(app_label=app_name, model_name="WorkflowTrigger")
DocumentType = apps.get_model(app_label=app_name, model_name="DocumentType")
Correspondent = apps.get_model(app_label=app_name, model_name="Correspondent")
StoragePath = apps.get_model(app_label=app_name, model_name="StoragePath")
Tag = apps.get_model(app_label=app_name, model_name="Tag")
CustomField = apps.get_model(app_label=app_name, model_name="CustomField")
MailRule = apps.get_model(app_label="paperless_mail", model_name="MailRule")
User = apps.get_model(app_label="auth", model_name="User")
Group = apps.get_model(app_label="auth", model_name="Group")
with transaction.atomic():
for template in ConsumptionTemplate.objects.all():
trigger = WorkflowTrigger(
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
type=1, # WorkflowTriggerType.CONSUMPTION
sources=template.sources,
filter_path=template.filter_path,
filter_filename=template.filter_filename,
@@ -143,10 +151,13 @@ def migrate_consumption_templates(apps, schema_editor):
def unmigrate_consumption_templates(apps, schema_editor):
model_name = "ConsumptionTemplate"
app_name = "documents"
ConsumptionTemplate = apps.get_model(app_label=app_name, model_name=model_name)
ConsumptionTemplate = apps.get_model(
app_label=app_name,
model_name="ConsumptionTemplate",
)
Workflow = apps.get_model(app_label=app_name, model_name="Workflow")
for workflow in Workflow.objects.all():
template = ConsumptionTemplate.objects.create(

View File

@@ -575,7 +575,11 @@ def run_workflow(
else ""
),
timezone.localtime(document.added),
document.original_filename,
(
document.original_filename
if document.original_filename is not None
else ""
),
timezone.localtime(document.created),
)
except Exception:

View File

@@ -1,6 +1,7 @@
import json
from unittest import mock
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from rest_framework import status
from rest_framework.test import APITestCase
@@ -310,17 +311,77 @@ class TestBulkEditObjects(APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(StoragePath.objects.count(), 0)
def test_bulk_edit_object_permissions_insufficient_perms(self):
def test_bulk_edit_object_permissions_insufficient_global_perms(self):
"""
GIVEN:
- Objects owned by user other than logged in user
- Existing objects, user does not have global delete permissions
WHEN:
- bulk_edit_objects API endpoint is called with delete operation
THEN:
- User is not able to delete objects
"""
self.t1.owner = User.objects.get(username="temp_admin")
self.t1.save()
self.client.force_authenticate(user=self.user1)
response = self.client.post(
"/api/bulk_edit_objects/",
json.dumps(
{
"objects": [self.t1.id, self.t2.id],
"object_type": "tags",
"operation": "delete",
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.content, b"Insufficient permissions")
def test_bulk_edit_object_permissions_sufficient_global_perms(self):
"""
GIVEN:
- Existing objects, user does have global delete permissions
WHEN:
- bulk_edit_objects API endpoint is called with delete operation
THEN:
- User is able to delete objects
"""
self.user1.user_permissions.add(
*Permission.objects.filter(codename="delete_tag"),
)
self.user1.save()
self.client.force_authenticate(user=self.user1)
response = self.client.post(
"/api/bulk_edit_objects/",
json.dumps(
{
"objects": [self.t1.id, self.t2.id],
"object_type": "tags",
"operation": "delete",
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_bulk_edit_object_permissions_insufficient_object_perms(self):
"""
GIVEN:
- Objects owned by user other than logged in user
WHEN:
- bulk_edit_objects API endpoint is called with delete operation
THEN:
- User is not able to delete objects
"""
self.t2.owner = User.objects.get(username="temp_admin")
self.t2.save()
self.user1.user_permissions.add(
*Permission.objects.filter(codename="delete_tag"),
)
self.user1.save()
self.client.force_authenticate(user=self.user1)
response = self.client.post(

View File

@@ -1419,7 +1419,15 @@ class BulkEditObjectsView(GenericAPIView, PassUserMixin):
objs = object_class.objects.filter(pk__in=object_ids)
if not user.is_superuser:
has_perms = all((obj.owner == user or obj.owner is None) for obj in objs)
model_name = object_class._meta.verbose_name
perm = (
f"documents.change_{model_name}"
if operation == "set_permissions"
else f"documents.delete_{model_name}"
)
has_perms = user.has_perm(perm) and all(
(obj.owner == user or obj.owner is None) for obj in objs
)
if not has_perms:
return HttpResponseForbidden("Insufficient permissions")

View File

@@ -14,7 +14,7 @@ from rest_framework.authtoken.models import Token
from rest_framework.filters import OrderingFilter
from rest_framework.generics import GenericAPIView
from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import DjangoObjectPermissions
from rest_framework.permissions import DjangoModelPermissions
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
@@ -171,7 +171,7 @@ class ApplicationConfigurationViewSet(ModelViewSet):
queryset = ApplicationConfiguration.objects
serializer_class = ApplicationConfigurationSerializer
permission_classes = (IsAuthenticated, DjangoObjectPermissions)
permission_classes = (IsAuthenticated, DjangoModelPermissions)
class DisconnectSocialAccountView(GenericAPIView):

View File

@@ -831,6 +831,7 @@ class MailAccountHandler(LoggingMixin):
input_doc = ConsumableDocument(
source=DocumentSource.MailFetch,
original_file=temp_filename,
mailrule_id=rule.pk,
)
doc_overrides = DocumentMetadataOverrides(
title=message.subject,