diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index c34bd61d0..56bce7c38 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -723,7 +723,7 @@ src/app/components/manage/settings/settings.component.ts - 594 + 597 @@ -2898,19 +2898,19 @@ src/app/components/manage/settings/settings.component.ts - 694 + 708 src/app/components/manage/settings/settings.component.ts - 754 + 768 src/app/components/manage/settings/settings.component.ts - 821 + 835 src/app/components/manage/settings/settings.component.ts - 884 + 898 @@ -2925,19 +2925,19 @@ src/app/components/manage/settings/settings.component.ts - 696 + 710 src/app/components/manage/settings/settings.component.ts - 756 + 770 src/app/components/manage/settings/settings.component.ts - 823 + 837 src/app/components/manage/settings/settings.component.ts - 886 + 900 @@ -4478,231 +4478,231 @@ Saved view "" deleted. src/app/components/manage/settings/settings.component.ts - 476 + 479 Settings saved src/app/components/manage/settings/settings.component.ts - 578 + 581 Settings were saved successfully. src/app/components/manage/settings/settings.component.ts - 579 + 582 Settings were saved successfully. Reload is required to apply some changes. src/app/components/manage/settings/settings.component.ts - 583 + 586 Reload now src/app/components/manage/settings/settings.component.ts - 584 + 587 Use system language src/app/components/manage/settings/settings.component.ts - 603 + 606 Use date format of display language src/app/components/manage/settings/settings.component.ts - 610 + 613 Error while storing settings on server. src/app/components/manage/settings/settings.component.ts - 630 + 633 Password has been changed, you will be logged out momentarily. src/app/components/manage/settings/settings.component.ts - 662 + 676 Saved user "". src/app/components/manage/settings/settings.component.ts - 669 + 683 Error saving user. src/app/components/manage/settings/settings.component.ts - 681 + 695 Confirm delete user account src/app/components/manage/settings/settings.component.ts - 692 + 706 This operation will permanently delete this user account. src/app/components/manage/settings/settings.component.ts - 693 + 707 Deleted user src/app/components/manage/settings/settings.component.ts - 702 + 716 Error deleting user. src/app/components/manage/settings/settings.component.ts - 710 + 724 Saved group "". src/app/components/manage/settings/settings.component.ts - 731 + 745 Error saving group. src/app/components/manage/settings/settings.component.ts - 741 + 755 Confirm delete user group src/app/components/manage/settings/settings.component.ts - 752 + 766 This operation will permanently delete this user group. src/app/components/manage/settings/settings.component.ts - 753 + 767 Deleted group src/app/components/manage/settings/settings.component.ts - 762 + 776 Error deleting group. src/app/components/manage/settings/settings.component.ts - 770 + 784 Saved account "". src/app/components/manage/settings/settings.component.ts - 796 + 810 Error saving account. src/app/components/manage/settings/settings.component.ts - 808 + 822 Confirm delete mail account src/app/components/manage/settings/settings.component.ts - 819 + 833 This operation will permanently delete this mail account. src/app/components/manage/settings/settings.component.ts - 820 + 834 Deleted mail account src/app/components/manage/settings/settings.component.ts - 829 + 843 Error deleting mail account. src/app/components/manage/settings/settings.component.ts - 838 + 852 Saved rule "". src/app/components/manage/settings/settings.component.ts - 859 + 873 Error saving rule. src/app/components/manage/settings/settings.component.ts - 871 + 885 Confirm delete mail rule src/app/components/manage/settings/settings.component.ts - 882 + 896 This operation will permanently delete this mail rule. src/app/components/manage/settings/settings.component.ts - 883 + 897 Deleted mail rule src/app/components/manage/settings/settings.component.ts - 892 + 906 Error deleting mail rule. src/app/components/manage/settings/settings.component.ts - 901 + 915 diff --git a/src-ui/src/app/components/manage/settings/settings.component.html b/src-ui/src/app/components/manage/settings/settings.component.html index 983395ea5..5090d531d 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.html +++ b/src-ui/src/app/components/manage/settings/settings.component.html @@ -266,8 +266,8 @@ {{account.imap_server}} - Edit - Delete + Edit + Delete @@ -303,8 +303,8 @@ {{(mailAccountService.getCached(rule.account) | async)?.name}} - Edit - Delete + Edit + Delete diff --git a/src-ui/src/app/components/manage/settings/settings.component.spec.ts b/src-ui/src/app/components/manage/settings/settings.component.spec.ts index f9f423fea..c4a9d4a4b 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.spec.ts +++ b/src-ui/src/app/components/manage/settings/settings.component.spec.ts @@ -48,8 +48,8 @@ const savedViews = [ { id: 2, name: 'view2' }, ] const users = [ - { id: 1, username: 'user1' }, - { id: 2, username: 'user2' }, + { id: 1, username: 'user1', is_superuser: false }, + { id: 2, username: 'user2', is_superuser: false }, ] const groups = [ { id: 1, name: 'group1' }, @@ -60,8 +60,8 @@ const mailAccounts = [ { id: 2, name: 'account2' }, ] const mailRules = [ - { id: 1, name: 'rule1' }, - { id: 2, name: 'rule2' }, + { id: 1, name: 'rule1', owner: 1 }, + { id: 2, name: 'rule2', owner: 2 }, ] describe('SettingsComponent', () => { @@ -75,6 +75,7 @@ describe('SettingsComponent', () => { let viewportScroller: ViewportScroller let toastService: ToastService let userService: UserService + let permissionsService: PermissionsService let groupService: GroupService let mailAccountService: MailAccountService let mailRuleService: MailRuleService @@ -90,17 +91,7 @@ describe('SettingsComponent', () => { CheckComponent, ColorComponent, ], - providers: [ - { - provide: PermissionsService, - useValue: { - currentUserCan: () => true, - }, - }, - CustomDatePipe, - DatePipe, - PermissionsGuard, - ], + providers: [CustomDatePipe, DatePipe, PermissionsGuard], imports: [ NgbModule, HttpClientTestingModule, @@ -117,6 +108,14 @@ describe('SettingsComponent', () => { toastService = TestBed.inject(ToastService) settingsService = TestBed.inject(SettingsService) userService = TestBed.inject(UserService) + permissionsService = TestBed.inject(PermissionsService) + jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) + jest + .spyOn(permissionsService, 'currentUserHasObjectPermissions') + .mockReturnValue(true) + jest + .spyOn(permissionsService, 'currentUserOwnsObject') + .mockReturnValue(true) jest.spyOn(userService, 'listAll').mockReturnValue( of({ all: users.map((u) => u.id), diff --git a/src-ui/src/app/components/manage/settings/settings.component.ts b/src-ui/src/app/components/manage/settings/settings.component.ts index c75867f7e..a49f2dd21 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.ts +++ b/src-ui/src/app/components/manage/settings/settings.component.ts @@ -45,6 +45,11 @@ import { MailRuleService } from 'src/app/services/rest/mail-rule.service' import { MailAccountEditDialogComponent } from '../../common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component' import { MailRuleEditDialogComponent } from '../../common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component' import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component' +import { ObjectWithPermissions } from 'src/app/data/object-with-permissions' +import { + PermissionAction, + PermissionsService, +} from 'src/app/services/permissions.service' enum SettingsNavIDs { General = 1, @@ -140,7 +145,8 @@ export class SettingsComponent private usersService: UserService, private groupsService: GroupService, private router: Router, - private modalService: NgbModal + private modalService: NgbModal, + private permissionsService: PermissionsService ) { super() this.settings.settingsSaved.subscribe(() => { @@ -642,6 +648,17 @@ export class SettingsComponent this.settingsForm.get('themeColor').patchValue('') } + userCanEdit(obj: ObjectWithPermissions): boolean { + return this.permissionsService.currentUserHasObjectPermissions( + PermissionAction.Change, + obj + ) + } + + userIsOwner(obj: ObjectWithPermissions): boolean { + return this.permissionsService.currentUserOwnsObject(obj) + } + editUser(user: PaperlessUser) { var modal = this.modalService.open(UserEditDialogComponent, { backdrop: 'static', diff --git a/src-ui/src/app/data/paperless-mail-account.ts b/src-ui/src/app/data/paperless-mail-account.ts index 484997213..5be0c3e20 100644 --- a/src-ui/src/app/data/paperless-mail-account.ts +++ b/src-ui/src/app/data/paperless-mail-account.ts @@ -1,4 +1,4 @@ -import { ObjectWithId } from './object-with-id' +import { ObjectWithPermissions } from './object-with-permissions' export enum IMAPSecurity { None = 1, @@ -6,7 +6,7 @@ export enum IMAPSecurity { STARTTLS = 3, } -export interface PaperlessMailAccount extends ObjectWithId { +export interface PaperlessMailAccount extends ObjectWithPermissions { name: string imap_server: string diff --git a/src-ui/src/app/data/paperless-mail-rule.ts b/src-ui/src/app/data/paperless-mail-rule.ts index 859fafc49..63351fe3e 100644 --- a/src-ui/src/app/data/paperless-mail-rule.ts +++ b/src-ui/src/app/data/paperless-mail-rule.ts @@ -1,4 +1,4 @@ -import { ObjectWithId } from './object-with-id' +import { ObjectWithPermissions } from './object-with-permissions' export enum MailFilterAttachmentType { Attachments = 1, @@ -31,7 +31,7 @@ export enum MailMetadataCorrespondentOption { FromCustom = 4, } -export interface PaperlessMailRule extends ObjectWithId { +export interface PaperlessMailRule extends ObjectWithPermissions { name: string account: number // PaperlessMailAccount.id diff --git a/src/paperless_mail/admin.py b/src/paperless_mail/admin.py index b035d14e4..b2eed5ce3 100644 --- a/src/paperless_mail/admin.py +++ b/src/paperless_mail/admin.py @@ -1,6 +1,7 @@ from django import forms from django.contrib import admin from django.utils.translation import gettext_lazy as _ +from guardian.admin import GuardedModelAdmin from paperless_mail.models import MailAccount from paperless_mail.models import MailRule @@ -31,7 +32,7 @@ class MailAccountAdminForm(forms.ModelForm): ] -class MailAccountAdmin(admin.ModelAdmin): +class MailAccountAdmin(GuardedModelAdmin): list_display = ("name", "imap_server", "username") fieldsets = [ @@ -45,7 +46,7 @@ class MailAccountAdmin(admin.ModelAdmin): form = MailAccountAdminForm -class MailRuleAdmin(admin.ModelAdmin): +class MailRuleAdmin(GuardedModelAdmin): radio_fields = { "attachment_type": admin.VERTICAL, "action": admin.VERTICAL, diff --git a/src/paperless_mail/serialisers.py b/src/paperless_mail/serialisers.py index 41dea9033..bdecff11e 100644 --- a/src/paperless_mail/serialisers.py +++ b/src/paperless_mail/serialisers.py @@ -25,7 +25,6 @@ class MailAccountSerializer(OwnedObjectSerializer): class Meta: model = MailAccount - depth = 1 fields = [ "id", "name", @@ -36,6 +35,10 @@ class MailAccountSerializer(OwnedObjectSerializer): "password", "character_set", "is_token", + "owner", + "user_can_change", + "permissions", + "set_permissions", ] def update(self, instance, validated_data): @@ -67,7 +70,6 @@ class MailRuleSerializer(OwnedObjectSerializer): class Meta: model = MailRule - depth = 1 fields = [ "id", "name", @@ -89,6 +91,10 @@ class MailRuleSerializer(OwnedObjectSerializer): "order", "attachment_type", "consumption_scope", + "owner", + "user_can_change", + "permissions", + "set_permissions", ] def update(self, instance, validated_data): diff --git a/src/paperless_mail/tests/test_api.py b/src/paperless_mail/tests/test_api.py index 28a369c6c..a9e88e4ad 100644 --- a/src/paperless_mail/tests/test_api.py +++ b/src/paperless_mail/tests/test_api.py @@ -1,7 +1,9 @@ import json from unittest import mock +from django.contrib.auth.models import Permission from django.contrib.auth.models import User +from guardian.shortcuts import assign_perm from rest_framework import status from rest_framework.test import APITestCase @@ -27,7 +29,9 @@ class TestAPIMailAccounts(DirectoriesMixin, APITestCase): super().setUp() - self.user = User.objects.create_superuser(username="temp_admin") + self.user = User.objects.create_user(username="temp_admin") + self.user.user_permissions.add(*Permission.objects.all()) + self.user.save() self.client.force_authenticate(user=self.user) def test_get_mail_accounts(self): @@ -266,6 +270,73 @@ class TestAPIMailAccounts(DirectoriesMixin, APITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["success"], True) + def test_get_mail_accounts_owner_aware(self): + """ + GIVEN: + - Configured accounts with different users + WHEN: + - API call is made to get mail accounts + THEN: + - Only unowned, owned by user or granted accounts are provided + """ + + user2 = User.objects.create_user(username="temp_admin2") + + account1 = MailAccount.objects.create( + name="Email1", + username="username1", + password="password1", + imap_server="server.example.com", + imap_port=443, + imap_security=MailAccount.ImapSecurity.SSL, + character_set="UTF-8", + ) + + account2 = MailAccount.objects.create( + name="Email2", + username="username2", + password="password2", + imap_server="server.example.com", + imap_port=443, + imap_security=MailAccount.ImapSecurity.SSL, + character_set="UTF-8", + ) + account2.owner = self.user + account2.save() + + account3 = MailAccount.objects.create( + name="Email3", + username="username3", + password="password3", + imap_server="server.example.com", + imap_port=443, + imap_security=MailAccount.ImapSecurity.SSL, + character_set="UTF-8", + ) + account3.owner = user2 + account3.save() + + account4 = MailAccount.objects.create( + name="Email4", + username="username4", + password="password4", + imap_server="server.example.com", + imap_port=443, + imap_security=MailAccount.ImapSecurity.SSL, + character_set="UTF-8", + ) + account4.owner = user2 + account4.save() + assign_perm("view_mailaccount", self.user, account4) + + response = self.client.get(self.ENDPOINT) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], 3) + self.assertEqual(response.data["results"][0]["name"], account1.name) + self.assertEqual(response.data["results"][1]["name"], account2.name) + self.assertEqual(response.data["results"][2]["name"], account4.name) + class TestAPIMailRules(DirectoriesMixin, APITestCase): ENDPOINT = "/api/mail_rules/" @@ -273,7 +344,9 @@ class TestAPIMailRules(DirectoriesMixin, APITestCase): def setUp(self): super().setUp() - self.user = User.objects.create_superuser(username="temp_admin") + self.user = User.objects.create_user(username="temp_admin") + self.user.user_permissions.add(*Permission.objects.all()) + self.user.save() self.client.force_authenticate(user=self.user) def test_get_mail_rules(self): @@ -533,3 +606,72 @@ class TestAPIMailRules(DirectoriesMixin, APITestCase): returned_rule1 = MailRule.objects.get(pk=rule1.pk) self.assertEqual(returned_rule1.name, "Updated Name 1") self.assertEqual(returned_rule1.action, MailRule.MailAction.DELETE) + + def test_get_mail_rules_owner_aware(self): + """ + GIVEN: + - Configured rules with different users + WHEN: + - API call is made to get mail rules + THEN: + - Only unowned, owned by user or granted mail rules are provided + """ + + user2 = User.objects.create_user(username="temp_admin2") + + account1 = MailAccount.objects.create( + name="Email1", + username="username1", + password="password1", + imap_server="server.example.com", + imap_port=443, + imap_security=MailAccount.ImapSecurity.SSL, + character_set="UTF-8", + ) + + rule1 = MailRule.objects.create( + name="Rule1", + account=account1, + folder="INBOX", + filter_from="from@example1.com", + order=0, + ) + + rule2 = MailRule.objects.create( + name="Rule2", + account=account1, + folder="INBOX", + filter_from="from@example2.com", + order=1, + ) + rule2.owner = self.user + rule2.save() + + rule3 = MailRule.objects.create( + name="Rule3", + account=account1, + folder="INBOX", + filter_from="from@example3.com", + order=2, + ) + rule3.owner = user2 + rule3.save() + + rule4 = MailRule.objects.create( + name="Rule4", + account=account1, + folder="INBOX", + filter_from="from@example4.com", + order=3, + ) + rule4.owner = user2 + rule4.save() + assign_perm("view_mailrule", self.user, rule4) + + response = self.client.get(self.ENDPOINT) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], 3) + self.assertEqual(response.data["results"][0]["name"], rule1.name) + self.assertEqual(response.data["results"][1]["name"], rule2.name) + self.assertEqual(response.data["results"][2]["name"], rule4.name) diff --git a/src/paperless_mail/views.py b/src/paperless_mail/views.py index 15346b920..e4a973c78 100644 --- a/src/paperless_mail/views.py +++ b/src/paperless_mail/views.py @@ -7,6 +7,8 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet +from documents.filters import ObjectOwnedOrGrantedPermissionsFilter +from documents.permissions import PaperlessObjectPermissions from documents.views import PassUserMixin from paperless.views import StandardPagination from paperless_mail.mail import MailError @@ -24,7 +26,8 @@ class MailAccountViewSet(ModelViewSet, PassUserMixin): queryset = MailAccount.objects.all().order_by("pk") serializer_class = MailAccountSerializer pagination_class = StandardPagination - permission_classes = (IsAuthenticated,) + permission_classes = (IsAuthenticated, PaperlessObjectPermissions) + filter_backends = (ObjectOwnedOrGrantedPermissionsFilter,) class MailRuleViewSet(ModelViewSet, PassUserMixin): @@ -33,7 +36,8 @@ class MailRuleViewSet(ModelViewSet, PassUserMixin): queryset = MailRule.objects.all().order_by("order") serializer_class = MailRuleSerializer pagination_class = StandardPagination - permission_classes = (IsAuthenticated,) + permission_classes = (IsAuthenticated, PaperlessObjectPermissions) + filter_backends = (ObjectOwnedOrGrantedPermissionsFilter,) class MailAccountTestView(GenericAPIView):