mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-31 10:37:12 -04:00 
			
		
		
		
	Unify API perm endpoint to set_permissions, initial frontend support for doc sharing
				
					
				
			This commit is contained in:
		
							parent
							
								
									2973e4672a
								
							
						
					
					
						commit
						79da613cb6
					
				| @ -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)" | ||||
|  | ||||
| @ -47,6 +47,9 @@ export class SelectComponent extends AbstractInputComponent<number> { | ||||
|   @Input() | ||||
|   multiple: boolean = false | ||||
| 
 | ||||
|   @Input() | ||||
|   bindLabel: string = 'name' | ||||
| 
 | ||||
|   @Output() | ||||
|   createNew = new EventEmitter<string>() | ||||
| 
 | ||||
|  | ||||
| @ -170,12 +170,23 @@ | ||||
|                         </div> | ||||
|                     </ng-template> | ||||
|                 </li> | ||||
| 
 | ||||
|                 <li [ngbNavItem]="5" *ngIf="commentsEnabled"> | ||||
|                     <a ngbNavLink i18n>Comments</a> | ||||
|                     <ng-template ngbNavContent> | ||||
|                         <app-document-comments [documentId]="documentId"></app-document-comments> | ||||
|                     </ng-template> | ||||
|                 </li> | ||||
| 
 | ||||
|                 <li [ngbNavItem]="6"> | ||||
|                     <a ngbNavLink i18n>Permissions</a> | ||||
|                     <ng-template ngbNavContent> | ||||
|                         <div formGroupName="set_permissions"> | ||||
|                             <app-input-select i18n-title title="Users can view" [items]="users" [bindLabel]="'username'" multiple="true" formControlName="view"></app-input-select> | ||||
|                             <app-input-select i18n-title title="Users can edit" [items]="users" [bindLabel]="'username'" multiple="true" formControlName="change"></app-input-select> | ||||
|                         </div> | ||||
|                     </ng-template> | ||||
|                 </li> | ||||
|             </ul> | ||||
| 
 | ||||
|             <div [ngbNavOutlet]="nav" class="mt-2"></div> | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
							
								
								
									
										8
									
								
								src-ui/src/app/data/object-with-permissions.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src-ui/src/app/data/object-with-permissions.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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]> | ||||
| } | ||||
| @ -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<PaperlessCorrespondent> | ||||
| 
 | ||||
|   correspondent?: number | ||||
|  | ||||
| @ -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<T extends ObjectWithId> { | ||||
|   protected baseUrl: string = environment.apiBaseUrl | ||||
|  | ||||
| @ -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.none() | ||||
|         if user_ids is not None: | ||||
|             users = User.objects.filter(id__in=user_ids) | ||||
|         if not users.count() == len(users): | ||||
|             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,18 +129,22 @@ 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, | ||||
|     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, | ||||
|                 ) | ||||
|         else: | ||||
|             for user in users: | ||||
|                 remove_perm( | ||||
|                     f"{type}_{object.__class__.__name__.lower()}", | ||||
|                 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, | ||||
|                     ) | ||||
| @ -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): | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user