From 79da613cb6636b1a2f871b968ece1ffb9eb95e99 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 7 Dec 2022 00:36:31 -0800 Subject: [PATCH] Unify API perm endpoint to `set_permissions`, initial frontend support for doc sharing --- .../common/input/select/select.component.html | 2 +- .../common/input/select/select.component.ts | 3 + .../document-detail.component.html | 11 ++ .../document-detail.component.ts | 33 +++- src-ui/src/app/data/matching-model.ts | 4 +- .../src/app/data/object-with-permissions.ts | 8 + src-ui/src/app/data/paperless-document.ts | 4 +- .../rest/abstract-paperless-service.ts | 2 + src/documents/serialisers.py | 154 ++++++------------ src/documents/tests/test_api.py | 2 +- src/documents/views.py | 1 + src/paperless/views.py | 4 +- 12 files changed, 111 insertions(+), 117 deletions(-) create mode 100644 src-ui/src/app/data/object-with-permissions.ts diff --git a/src-ui/src/app/components/common/input/select/select.component.html b/src-ui/src/app/components/common/input/select/select.component.html index 83e642bed..d775f4ffa 100644 --- a/src-ui/src/app/components/common/input/select/select.component.html +++ b/src-ui/src/app/components/common/input/select/select.component.html @@ -12,7 +12,7 @@ i18n-addTagText="Used for both types, correspondents, storage paths" [placeholder]="placeholder" [multiple]="multiple" - bindLabel="name" + [bindLabel]="bindLabel" bindValue="id" (change)="onChange(value)" (search)="onSearch($event)" diff --git a/src-ui/src/app/components/common/input/select/select.component.ts b/src-ui/src/app/components/common/input/select/select.component.ts index 9ae361387..877b0f78d 100644 --- a/src-ui/src/app/components/common/input/select/select.component.ts +++ b/src-ui/src/app/components/common/input/select/select.component.ts @@ -47,6 +47,9 @@ export class SelectComponent extends AbstractInputComponent { @Input() multiple: boolean = false + @Input() + bindLabel: string = 'name' + @Output() createNew = new EventEmitter() diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html index 509e1b874..9ffc00dd4 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.html +++ b/src-ui/src/app/components/document-detail/document-detail.component.html @@ -170,12 +170,23 @@ +
  • Comments
  • + +
  • + Permissions + +
    + + +
    +
    +
  • diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index 624ea7720..72f4be70b 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -40,6 +40,8 @@ import { PermissionsService, PermissionType, } from 'src/app/services/permissions.service' +import { UserService } from 'src/app/services/rest/user.service' +import { PaperlessUser } from 'src/app/data/paperless-user' @Component({ selector: 'app-document-detail', @@ -73,6 +75,7 @@ export class DocumentDetailComponent correspondents: PaperlessCorrespondent[] documentTypes: PaperlessDocumentType[] storagePaths: PaperlessStoragePath[] + users: PaperlessUser[] documentForm: FormGroup = new FormGroup({ title: new FormControl(''), @@ -83,6 +86,10 @@ export class DocumentDetailComponent storage_path: new FormControl(), archive_serial_number: new FormControl(), tags: new FormControl([]), + set_permissions: new FormGroup({ + view: new FormControl(null), + change: new FormControl(null), + }), }) previewCurrentPage: number = 1 @@ -127,7 +134,8 @@ export class DocumentDetailComponent private toastService: ToastService, private settings: SettingsService, private storagePathService: StoragePathService, - private permissionsService: PermissionsService + private permissionsService: PermissionsService, + private userService: UserService ) {} titleKeyUp(event) { @@ -167,6 +175,11 @@ export class DocumentDetailComponent .pipe(first()) .subscribe((result) => (this.storagePaths = result.results)) + this.userService + .listAll() + .pipe(first()) + .subscribe((result) => (this.users = result.results)) + this.route.paramMap .pipe( takeUntil(this.unsubscribeNotifier), @@ -230,6 +243,14 @@ export class DocumentDetailComponent storage_path: doc.storage_path, archive_serial_number: doc.archive_serial_number, tags: [...doc.tags], + set_permissions: { + view: doc.permissions + .filter((p) => (p[1] as string).includes('view')) + .map((p) => p[0]), + change: doc.permissions + .filter((p) => (p[1] as string).includes('change')) + .map((p) => p[0]), + }, }) this.isDirty$ = dirtyCheck( @@ -284,6 +305,14 @@ export class DocumentDetailComponent }, }) this.title = this.documentTitlePipe.transform(doc.title) + doc['set_permissions'] = { + view: doc.permissions + .filter((p) => (p[1] as string).includes('view')) + .map((p) => p[0]), + change: doc.permissions + .filter((p) => (p[1] as string).includes('change')) + .map((p) => p[0]), + } this.documentForm.patchValue(doc) } @@ -376,7 +405,7 @@ export class DocumentDetailComponent .update(this.document) .pipe(first()) .subscribe({ - next: (result) => { + next: () => { this.close() this.networkActive = false this.error = null diff --git a/src-ui/src/app/data/matching-model.ts b/src-ui/src/app/data/matching-model.ts index 8ce05528e..387625b54 100644 --- a/src-ui/src/app/data/matching-model.ts +++ b/src-ui/src/app/data/matching-model.ts @@ -1,4 +1,4 @@ -import { ObjectWithId } from './object-with-id' +import { ObjectWithPermissions } from './object-with-permissions' export const MATCH_ANY = 1 export const MATCH_ALL = 2 @@ -41,7 +41,7 @@ export const MATCHING_ALGORITHMS = [ }, ] -export interface MatchingModel extends ObjectWithId { +export interface MatchingModel extends ObjectWithPermissions { name?: string slug?: string diff --git a/src-ui/src/app/data/object-with-permissions.ts b/src-ui/src/app/data/object-with-permissions.ts new file mode 100644 index 000000000..786cb16fa --- /dev/null +++ b/src-ui/src/app/data/object-with-permissions.ts @@ -0,0 +1,8 @@ +import { ObjectWithId } from './object-with-id' +import { PaperlessUser } from './paperless-user' + +export interface ObjectWithPermissions extends ObjectWithId { + user?: PaperlessUser + + permissions?: Array<[number, string]> +} diff --git a/src-ui/src/app/data/paperless-document.ts b/src-ui/src/app/data/paperless-document.ts index 8b038d79e..9899c60ac 100644 --- a/src-ui/src/app/data/paperless-document.ts +++ b/src-ui/src/app/data/paperless-document.ts @@ -1,9 +1,9 @@ import { PaperlessCorrespondent } from './paperless-correspondent' -import { ObjectWithId } from './object-with-id' import { PaperlessTag } from './paperless-tag' import { PaperlessDocumentType } from './paperless-document-type' import { Observable } from 'rxjs' import { PaperlessStoragePath } from './paperless-storage-path' +import { ObjectWithPermissions } from './object-with-permissions' export interface SearchHit { score?: number @@ -12,7 +12,7 @@ export interface SearchHit { highlights?: string } -export interface PaperlessDocument extends ObjectWithId { +export interface PaperlessDocument extends ObjectWithPermissions { correspondent$?: Observable correspondent?: number diff --git a/src-ui/src/app/services/rest/abstract-paperless-service.ts b/src-ui/src/app/services/rest/abstract-paperless-service.ts index 9a5664c9d..f7833c812 100644 --- a/src-ui/src/app/services/rest/abstract-paperless-service.ts +++ b/src-ui/src/app/services/rest/abstract-paperless-service.ts @@ -2,8 +2,10 @@ import { HttpClient, HttpParams } from '@angular/common/http' import { Observable } from 'rxjs' import { map, publishReplay, refCount } from 'rxjs/operators' import { ObjectWithId } from 'src/app/data/object-with-id' +import { PaperlessUser } from 'src/app/data/paperless-user' import { Results } from 'src/app/data/results' import { environment } from 'src/environments/environment' +import { PermissionAction, PermissionType } from '../permissions.service' export abstract class AbstractPaperlessService { protected baseUrl: string = environment.apiBaseUrl diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 234ef21da..43f3a758e 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -31,6 +31,7 @@ from .parsers import is_mime_type_supported from guardian.models import UserObjectPermission 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 @@ -91,55 +92,36 @@ class OwnedObjectSerializer(serializers.ModelSerializer): ).values_list("user", "permission__codename") return list(user_object_perms) - permissions = SerializerMethodField() + permissions = SerializerMethodField(read_only=True) - grant_permissions = serializers.DictField( - label="Grant permissions", + set_permissions = serializers.DictField( + label="Set permissions", allow_empty=True, required=False, write_only=True, ) def _validate_user_ids(self, user_ids): - users = User.objects.filter(id__in=user_ids) - if not users.count() == len(users): - raise serializers.ValidationError( - "Some users in don't exist or were specified twice.", - ) + users = User.objects.none() + if user_ids is not None: + users = User.objects.filter(id__in=user_ids) + if not users.count() == len(user_ids): + raise serializers.ValidationError( + "Some users in don't exist or were specified twice.", + ) return users - def validate_grant_permissions(self, grant_permissions): + def validate_set_permissions(self, set_permissions): user_dict = { "view": User.objects.none(), "change": User.objects.none(), } - if grant_permissions is not None: - if "view" in grant_permissions: - view_list = grant_permissions["view"] + if set_permissions is not None: + if "view" in set_permissions: + view_list = set_permissions["view"] user_dict["view"] = self._validate_user_ids(view_list) - if "change" in grant_permissions: - change_list = grant_permissions["change"] - user_dict["change"] = self._validate_user_ids(change_list) - return user_dict - - revoke_permissions = serializers.DictField( - label="Revoke permissions", - allow_empty=True, - required=False, - write_only=True, - ) - - def validate_revoke_permissions(self, revoke_permissions): - user_dict = { - "view": User.objects.none(), - "change": User.objects.none(), - } - if revoke_permissions is not None: - if "view" in revoke_permissions: - view_list = revoke_permissions["view"] - user_dict["view"] = self._validate_user_ids(view_list) - if "change" in revoke_permissions: - change_list = revoke_permissions["change"] + if "change" in set_permissions: + change_list = set_permissions["change"] user_dict["change"] = self._validate_user_ids(change_list) return user_dict @@ -147,21 +129,25 @@ class OwnedObjectSerializer(serializers.ModelSerializer): self.user = kwargs.pop("user", None) return super().__init__(*args, **kwargs) - def _adjust_permissions(self, users, object, type="view", grant=True): - if grant: - for user in users: - assign_perm( - f"{type}_{object.__class__.__name__.lower()}", - user, - object, - ) - else: - for user in users: - remove_perm( - f"{type}_{object.__class__.__name__.lower()}", - user, - object, - ) + def _set_permissions(self, permissions, object): + for action in permissions: + permission = f"{action}_{object.__class__.__name__.lower()}" + users_to_add = permissions[action] + 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, + ) def create(self, validated_data): if self.user and ( @@ -169,55 +155,13 @@ class OwnedObjectSerializer(serializers.ModelSerializer): ): validated_data["owner"] = self.user instance = super().create(validated_data) - if "grant_permissions" in validated_data: - self._adjust_permissions( - validated_data["grant_permissions"]["view"], - instance, - ) - self._adjust_permissions( - validated_data["grant_permissions"]["change"], - instance, - "change", - ) - if "revoke_permissions" in validated_data: - self._adjust_permissions( - validated_data["revoke_permissions"]["view"], - instance, - "view", - False, - ) - self._adjust_permissions( - validated_data["revoke_permissions"]["change"], - instance, - "change", - False, - ) + if "set_permissions" in validated_data: + self._set_permissions(validated_data["set_permissions"], instance) return instance def update(self, instance, validated_data): - if "grant_permissions" in validated_data: - self._adjust_permissions( - validated_data["grant_permissions"]["view"], - instance, - ) - self._adjust_permissions( - validated_data["grant_permissions"]["change"], - instance, - "change", - ) - if "revoke_permissions" in validated_data: - self._adjust_permissions( - validated_data["revoke_permissions"]["view"], - instance, - "view", - False, - ) - self._adjust_permissions( - validated_data["revoke_permissions"]["change"], - instance, - "change", - False, - ) + if "set_permissions" in validated_data: + self._set_permissions(validated_data["set_permissions"], instance) return super().update(instance, validated_data) @@ -238,8 +182,7 @@ class CorrespondentSerializer(MatchingModelSerializer, OwnedObjectSerializer): "last_correspondence", "owner", "permissions", - "grant_permissions", - "revoke_permissions", + "set_permissions", ) @@ -256,8 +199,7 @@ class DocumentTypeSerializer(MatchingModelSerializer, OwnedObjectSerializer): "document_count", "owner", "permissions", - "grant_permissions", - "revoke_permissions", + "set_permissions", ) @@ -342,8 +284,7 @@ class TagSerializer(MatchingModelSerializer, OwnedObjectSerializer): "document_count", "owner", "permissions", - "grant_permissions", - "revoke_permissions", + "set_permissions", ) def validate_color(self, color): @@ -426,8 +367,7 @@ class DocumentSerializer(DynamicFieldsModelSerializer, OwnedObjectSerializer): "archived_file_name", "owner", "permissions", - "grant_permissions", - "revoke_permissions", + "set_permissions", ) @@ -454,8 +394,7 @@ class SavedViewSerializer(OwnedObjectSerializer): "filter_rules", "owner", "permissions", - "grant_permissions", - "revoke_permissions", + "set_permissions", ] def update(self, instance, validated_data): @@ -749,8 +688,7 @@ class StoragePathSerializer(MatchingModelSerializer, OwnedObjectSerializer): "document_count", "owner", "permissions", - "grant_permissions", - "revoke_permissions", + "set_permissions", ) def validate_path(self, path): diff --git a/src/documents/tests/test_api.py b/src/documents/tests/test_api.py index 2b937867c..402c1023f 100644 --- a/src/documents/tests/test_api.py +++ b/src/documents/tests/test_api.py @@ -3015,7 +3015,7 @@ class TestApiUser(APITestCase): response = self.client.get(self.ENDPOINT) self.assertEqual(response.status_code, 200) - self.assertEqual(response.data["count"], 3) # AnonymousUser + self.assertEqual(response.data["count"], 2) returned_user2 = response.data["results"][2] self.assertEqual(returned_user2["username"], user1.username) diff --git a/src/documents/views.py b/src/documents/views.py index 8af4e1477..8b1dfedde 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -256,6 +256,7 @@ class DocumentViewSet( else: fields = None serializer_class = self.get_serializer_class() + kwargs.setdefault("user", self.request.user) # PassUserMixin kwargs.setdefault("context", self.get_serializer_context()) kwargs.setdefault("fields", fields) return serializer_class(*args, **kwargs) diff --git a/src/paperless/views.py b/src/paperless/views.py index 7ff1462d3..fea2d7bf5 100644 --- a/src/paperless/views.py +++ b/src/paperless/views.py @@ -39,7 +39,9 @@ class FaviconView(View): class UserViewSet(ModelViewSet): model = User - queryset = User.objects.exclude(username="consumer").order_by(Lower("username")) + queryset = User.objects.exclude( + username__in=["consumer", "AnonymousUser"], + ).order_by(Lower("username")) serializer_class = UserSerializer pagination_class = StandardPagination