diff --git a/src-ui/src/app/app.component.ts b/src-ui/src/app/app.component.ts index 320e9393a..01eac1297 100644 --- a/src-ui/src/app/app.component.ts +++ b/src-ui/src/app/app.component.ts @@ -81,10 +81,10 @@ export class AppComponent implements OnInit, OnDestroy { this.showNotification(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS) ) { if ( - this.permissionsService.currentUserCan({ - action: PermissionAction.View, - type: PermissionType.Document, - }) + this.permissionsService.currentUserCan( + PermissionAction.View, + PermissionType.Document + ) ) { this.toastService.show({ title: $localize`Document added`, @@ -246,10 +246,10 @@ export class AppComponent implements OnInit, OnDestroy { public get dragDropEnabled(): boolean { return ( !this.router.url.includes('dashboard') && - this.permissionsService.currentUserCan({ - action: PermissionAction.Add, - type: PermissionType.Document, - }) + this.permissionsService.currentUserCan( + PermissionAction.Add, + PermissionType.Document + ) ) } diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index 60a6f4844..9e97ac90b 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -84,6 +84,10 @@ import { GroupEditDialogComponent } from './components/common/edit-dialog/group- import { PermissionsSelectComponent } from './components/common/permissions-select/permissions-select.component' import { MailAccountEditDialogComponent } from './components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component' import { MailRuleEditDialogComponent } from './components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component' +import { PermissionsUserComponent } from './components/common/input/permissions-user/permissions-user.component' +import { PermissionsGroupComponent } from './components/common/input/permissions-group/permissions-group.component' +import { IfOwnerDirective } from './directives/if-owner.directive' +import { IfObjectPermissionsDirective } from './directives/if-object-permissions.directive' import localeBe from '@angular/common/locales/be' import localeCs from '@angular/common/locales/cs' @@ -104,9 +108,6 @@ 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 { ShareUserComponent } from './components/common/input/share-user/share-user.component' -import { IfOwnerDirective } from './directives/if-owner.directive' -import { IfObjectPermissionsDirective } from './directives/if-object-permissions.directive' registerLocaleData(localeBe) registerLocaleData(localeCs) @@ -198,7 +199,8 @@ function initializeApp(settings: SettingsService) { PermissionsSelectComponent, MailAccountEditDialogComponent, MailRuleEditDialogComponent, - ShareUserComponent, + PermissionsUserComponent, + PermissionsGroupComponent, IfOwnerDirective, IfObjectPermissionsDirective, ], diff --git a/src-ui/src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html b/src-ui/src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html index 83f24812f..3867146a3 100644 --- a/src-ui/src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html +++ b/src-ui/src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html @@ -11,11 +11,19 @@ -
+
Permissions
- - +
View
+
+ + +
+
Edit
+
+ + +
diff --git a/src-ui/src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.ts index a7c3eb606..5320def27 100644 --- a/src-ui/src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.ts +++ b/src-ui/src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.ts @@ -31,8 +31,14 @@ export class CorrespondentEditDialogComponent extends EditDialogComponent -
+
Permissions
- - +
View
+
+ + +
+
Edit
+
+ + +
diff --git a/src-ui/src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.ts index ef4d0a864..81854df34 100644 --- a/src-ui/src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.ts +++ b/src-ui/src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.ts @@ -31,8 +31,14 @@ export class DocumentTypeEditDialogComponent extends EditDialogComponent (p[1] as string).includes('view')) - .map((p) => p[0]), - change: (this.object as ObjectWithPermissions).permissions - .filter((p) => (p[1] as string).includes('change')) - .map((p) => p[0]), - } + this.object['set_permissions'] = this.object['permissions'] } this.objectForm.patchValue(this.object) } diff --git a/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html b/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html index a0e141907..24379a52d 100644 --- a/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html +++ b/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html @@ -16,11 +16,19 @@ -
+
Permissions
- - +
View
+
+ + +
+
Edit
+
+ + +
diff --git a/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts index 7c4898703..42d533c96 100644 --- a/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts +++ b/src-ui/src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.ts @@ -42,8 +42,14 @@ export class StoragePathEditDialogComponent extends EditDialogComponent -
+
Permissions
- - +
View
+
+ + +
+
Edit
+
+ + +
diff --git a/src-ui/src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.ts index 0414052a0..8b79d36bc 100644 --- a/src-ui/src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.ts +++ b/src-ui/src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.ts @@ -34,8 +34,14 @@ export class TagEditDialogComponent extends EditDialogComponent { match: new FormControl(''), is_insensitive: new FormControl(true), set_permissions: new FormGroup({ - view: new FormControl(null), - change: new FormControl(null), + view: new FormGroup({ + users: new FormControl(null), + groups: new FormControl(null), + }), + change: new FormGroup({ + users: new FormControl(null), + groups: new FormControl(null), + }), }), }) } diff --git a/src-ui/src/app/components/common/input/permissions-group/permissions-group.component.html b/src-ui/src/app/components/common/input/permissions-group/permissions-group.component.html new file mode 100644 index 000000000..1b74b1eb4 --- /dev/null +++ b/src-ui/src/app/components/common/input/permissions-group/permissions-group.component.html @@ -0,0 +1,15 @@ +
+ +
+ + +
+ {{hint}} +
diff --git a/src-ui/src/app/components/common/input/share-user/share-user.component.scss b/src-ui/src/app/components/common/input/permissions-group/permissions-group.component.scss similarity index 100% rename from src-ui/src/app/components/common/input/share-user/share-user.component.scss rename to src-ui/src/app/components/common/input/permissions-group/permissions-group.component.scss diff --git a/src-ui/src/app/components/common/input/permissions-group/permissions-group.component.ts b/src-ui/src/app/components/common/input/permissions-group/permissions-group.component.ts new file mode 100644 index 000000000..bef172747 --- /dev/null +++ b/src-ui/src/app/components/common/input/permissions-group/permissions-group.component.ts @@ -0,0 +1,48 @@ +import { Component, forwardRef, Input, OnInit } from '@angular/core' +import { NG_VALUE_ACCESSOR } from '@angular/forms' +import { first } from 'rxjs/operators' +import { PaperlessGroup } from 'src/app/data/paperless-group' +import { GroupService } from 'src/app/services/rest/group.service' +import { SettingsService } from 'src/app/services/settings.service' +import { AbstractInputComponent } from '../abstract-input' + +@Component({ + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => PermissionsGroupComponent), + multi: true, + }, + ], + selector: 'app-permissions-group', + templateUrl: './permissions-group.component.html', + styleUrls: ['./permissions-group.component.scss'], +}) +export class PermissionsGroupComponent + extends AbstractInputComponent + implements OnInit +{ + groups: PaperlessGroup[] + + @Input() + type: string + + constructor(groupService: GroupService, settings: SettingsService) { + super() + groupService + .listAll() + .pipe(first()) + .subscribe((result) => (this.groups = result.results)) + } + + ngOnInit(): void { + if (this.type == 'view') { + this.title = $localize`Groups can view` + } else if (this.type == 'change') { + this.title = $localize`Groups can edit` + this.hint = $localize`Edit permissions also grant viewing permissions` + } + + super.ngOnInit() + } +} diff --git a/src-ui/src/app/components/common/input/share-user/share-user.component.html b/src-ui/src/app/components/common/input/permissions-user/permissions-user.component.html similarity index 100% rename from src-ui/src/app/components/common/input/share-user/share-user.component.html rename to src-ui/src/app/components/common/input/permissions-user/permissions-user.component.html diff --git a/src-ui/src/app/components/common/input/permissions-user/permissions-user.component.scss b/src-ui/src/app/components/common/input/permissions-user/permissions-user.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src-ui/src/app/components/common/input/share-user/share-user.component.ts b/src-ui/src/app/components/common/input/permissions-user/permissions-user.component.ts similarity index 63% rename from src-ui/src/app/components/common/input/share-user/share-user.component.ts rename to src-ui/src/app/components/common/input/permissions-user/permissions-user.component.ts index d132b3964..87ce08f08 100644 --- a/src-ui/src/app/components/common/input/share-user/share-user.component.ts +++ b/src-ui/src/app/components/common/input/permissions-user/permissions-user.component.ts @@ -3,21 +3,22 @@ import { NG_VALUE_ACCESSOR } from '@angular/forms' import { first } from 'rxjs/operators' import { PaperlessUser } from 'src/app/data/paperless-user' import { UserService } from 'src/app/services/rest/user.service' +import { SettingsService } from 'src/app/services/settings.service' import { AbstractInputComponent } from '../abstract-input' @Component({ providers: [ { provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => ShareUserComponent), + useExisting: forwardRef(() => PermissionsUserComponent), multi: true, }, ], - selector: 'app-share-user', - templateUrl: './share-user.component.html', - styleUrls: ['./share-user.component.scss'], + selector: 'app-permissions-user', + templateUrl: './permissions-user.component.html', + styleUrls: ['./permissions-user.component.scss'], }) -export class ShareUserComponent +export class PermissionsUserComponent extends AbstractInputComponent implements OnInit { @@ -26,12 +27,17 @@ export class ShareUserComponent @Input() type: string - constructor(userService: UserService) { + constructor(userService: UserService, settings: SettingsService) { super() userService .listAll() .pipe(first()) - .subscribe((result) => (this.users = result.results)) + .subscribe( + (result) => + (this.users = result.results.filter( + (u) => u.id !== settings.currentUser.id + )) + ) } ngOnInit(): void { diff --git a/src-ui/src/app/components/common/permissions-select/permissions-select.component.ts b/src-ui/src/app/components/common/permissions-select/permissions-select.component.ts index 56ba619bb..673de6cb6 100644 --- a/src-ui/src/app/components/common/permissions-select/permissions-select.component.ts +++ b/src-ui/src/app/components/common/permissions-select/permissions-select.component.ts @@ -156,18 +156,18 @@ export class PermissionsSelectComponent if (this._inheritedPermissions.length == 0) return false else if (actionKey) { return this._inheritedPermissions.includes( - this.permissionsService.getPermissionCode({ - action: PermissionAction[actionKey], - type: PermissionType[typeKey], - }) + this.permissionsService.getPermissionCode( + PermissionAction[actionKey], + PermissionType[typeKey] + ) ) } else { return Object.values(PermissionAction).every((action) => { return this._inheritedPermissions.includes( - this.permissionsService.getPermissionCode({ - action: action as PermissionAction, - type: PermissionType[typeKey], - }) + this.permissionsService.getPermissionCode( + action as PermissionAction, + PermissionType[typeKey] + ) ) }) } 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 6b729ee4c..d2100b59c 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 @@ -5,7 +5,7 @@
of {{previewNumPages}}
-
-       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 8d564f4b5..740ef46f9 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 @@ -85,8 +85,14 @@ export class DocumentDetailComponent archive_serial_number: new FormControl(), tags: new FormControl([]), set_permissions: new FormGroup({ - view: new FormControl(null), - change: new FormControl(null), + view: new FormGroup({ + users: new FormControl(null), + groups: new FormControl(null), + }), + change: new FormGroup({ + users: new FormControl(null), + groups: new FormControl(null), + }), }), }) @@ -235,14 +241,7 @@ 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]), - }, + set_permissions: doc.permissions, }) this.isDirty$ = dirtyCheck( @@ -297,14 +296,7 @@ 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]), - } + doc['set_permissions'] = doc.permissions this.documentForm.patchValue(doc) if (!this.userCanEdit) this.documentForm.disable() } @@ -586,10 +578,10 @@ export class DocumentDetailComponent get commentsEnabled(): boolean { return ( this.settings.get(SETTINGS_KEYS.COMMENTS_ENABLED) && - this.permissionsService.currentUserCan({ - action: PermissionAction.View, - type: PermissionType.Document, - }) + this.permissionsService.currentUserCan( + PermissionAction.View, + PermissionType.Document + ) ) } diff --git a/src-ui/src/app/components/manage/management-list/management-list.component.ts b/src-ui/src/app/components/manage/management-list/management-list.component.ts index fd63a44b6..7695c4abb 100644 --- a/src-ui/src/app/components/manage/management-list/management-list.component.ts +++ b/src-ui/src/app/components/manage/management-list/management-list.component.ts @@ -222,9 +222,7 @@ export abstract class ManagementListComponent } userCanDelete(object: ObjectWithPermissions): boolean { - return ( - !object.owner || this.permissionsService.currentUserIsOwner(object.owner) - ) + return this.permissionsService.currentUserOwnsObject(object) } userCanEdit(object: ObjectWithPermissions): boolean { diff --git a/src-ui/src/app/data/object-with-permissions.ts b/src-ui/src/app/data/object-with-permissions.ts index a95ada157..dce0cc02a 100644 --- a/src-ui/src/app/data/object-with-permissions.ts +++ b/src-ui/src/app/data/object-with-permissions.ts @@ -1,8 +1,19 @@ import { ObjectWithId } from './object-with-id' import { PaperlessUser } from './paperless-user' +export interface PermissionsObject { + view: { + users: Array + groups: Array + } + change: { + users: Array + groups: Array + } +} + export interface ObjectWithPermissions extends ObjectWithId { owner?: PaperlessUser - permissions?: Array<[number, string]> + permissions?: PermissionsObject } diff --git a/src-ui/src/app/directives/if-object-permissions.directive.ts b/src-ui/src/app/directives/if-object-permissions.directive.ts index c897b848d..ccc90b70b 100644 --- a/src-ui/src/app/directives/if-object-permissions.directive.ts +++ b/src-ui/src/app/directives/if-object-permissions.directive.ts @@ -1,5 +1,6 @@ import { Directive, + EmbeddedViewRef, Input, OnChanges, OnInit, @@ -18,10 +19,12 @@ import { export class IfObjectPermissionsDirective implements OnInit, OnChanges { // The role the user must have @Input() - ifObjectPermissions: ObjectWithPermissions + ifObjectPermissions: { + object: ObjectWithPermissions + action: PermissionAction + } - @Input() - action: PermissionAction + createdView: EmbeddedViewRef /** * @param {ViewContainerRef} viewContainerRef -- The location where we need to render the templateRef @@ -36,13 +39,16 @@ export class IfObjectPermissionsDirective implements OnInit, OnChanges { public ngOnInit(): void { if ( - !this.ifObjectPermissions || + !this.ifObjectPermissions?.object || this.permissionsService.currentUserHasObjectPermissions( - this.action, - this.ifObjectPermissions + this.ifObjectPermissions.action, + this.ifObjectPermissions.object ) ) { - this.viewContainerRef.createEmbeddedView(this.templateRef) + if (!this.createdView) + this.createdView = this.viewContainerRef.createEmbeddedView( + this.templateRef + ) } else { this.viewContainerRef.clear() } diff --git a/src-ui/src/app/directives/if-owner.directive.ts b/src-ui/src/app/directives/if-owner.directive.ts index 86fcd3457..082cc1679 100644 --- a/src-ui/src/app/directives/if-owner.directive.ts +++ b/src-ui/src/app/directives/if-owner.directive.ts @@ -1,12 +1,13 @@ import { Directive, + EmbeddedViewRef, Input, OnChanges, OnInit, TemplateRef, ViewContainerRef, } from '@angular/core' -import { PaperlessUser } from '../data/paperless-user' +import { ObjectWithPermissions } from '../data/object-with-permissions' import { PermissionsService } from '../services/permissions.service' @Directive({ @@ -15,7 +16,9 @@ import { PermissionsService } from '../services/permissions.service' export class IfOwnerDirective implements OnInit, OnChanges { // The role the user must have @Input() - ifOwner: PaperlessUser + ifOwner: ObjectWithPermissions + + createdView: EmbeddedViewRef /** * @param {ViewContainerRef} viewContainerRef -- The location where we need to render the templateRef @@ -29,11 +32,11 @@ export class IfOwnerDirective implements OnInit, OnChanges { ) {} public ngOnInit(): void { - if ( - !this.ifOwner || - this.permissionsService.currentUserIsOwner(this.ifOwner) - ) { - this.viewContainerRef.createEmbeddedView(this.templateRef) + if (this.permissionsService.currentUserOwnsObject(this.ifOwner)) { + if (!this.createdView) + this.createdView = this.viewContainerRef.createEmbeddedView( + this.templateRef + ) } else { this.viewContainerRef.clear() } diff --git a/src-ui/src/app/directives/if-permissions.directive.ts b/src-ui/src/app/directives/if-permissions.directive.ts index da93b65fc..6dedaee25 100644 --- a/src-ui/src/app/directives/if-permissions.directive.ts +++ b/src-ui/src/app/directives/if-permissions.directive.ts @@ -6,17 +6,19 @@ import { TemplateRef, } from '@angular/core' import { - PaperlessPermission, + PermissionAction, PermissionsService, + PermissionType, } from '../services/permissions.service' @Directive({ selector: '[ifPermissions]', }) export class IfPermissionsDirective implements OnInit { - // The role the user must have @Input() - ifPermissions: Array | PaperlessPermission + ifPermissions: + | Array<{ action: PermissionAction; type: PermissionType }> + | { action: PermissionAction; type: PermissionType } /** * @param {ViewContainerRef} viewContainerRef -- The location where we need to render the templateRef @@ -33,8 +35,8 @@ export class IfPermissionsDirective implements OnInit { if ( [] .concat(this.ifPermissions) - .every((perm: PaperlessPermission) => - this.permissionsService.currentUserCan(perm) + .every((perm: { action: PermissionAction; type: PermissionType }) => + this.permissionsService.currentUserCan(perm.action, perm.type) ) ) { this.viewContainerRef.createEmbeddedView(this.templateRef) diff --git a/src-ui/src/app/guards/permissions.guard.ts b/src-ui/src/app/guards/permissions.guard.ts index 39536ed55..916408fe2 100644 --- a/src-ui/src/app/guards/permissions.guard.ts +++ b/src-ui/src/app/guards/permissions.guard.ts @@ -22,7 +22,10 @@ export class PermissionsGuard implements CanActivate { state: RouterStateSnapshot ): boolean | UrlTree { if ( - !this.permissionsService.currentUserCan(route.data.requiredPermission) + !this.permissionsService.currentUserCan( + route.data.requiredPermission.action, + route.data.requiredPermission.type + ) ) { this.toastService.showError( $localize`You don't have permissions to do that` diff --git a/src-ui/src/app/services/permissions.service.ts b/src-ui/src/app/services/permissions.service.ts index 0f7edee22..b9bad0d96 100644 --- a/src-ui/src/app/services/permissions.service.ts +++ b/src-ui/src/app/services/permissions.service.ts @@ -25,11 +25,6 @@ export enum PermissionType { Admin = '%s_logentry', } -export interface PaperlessPermission { - action: PermissionAction - type: PermissionType -} - @Injectable({ providedIn: 'root', }) @@ -42,25 +37,34 @@ export class PermissionsService { this.currentUser = currentUser } - public currentUserCan(permission: PaperlessPermission): boolean { - return this.permissions.includes(this.getPermissionCode(permission)) + public currentUserCan( + action: PermissionAction, + type: PermissionType + ): boolean { + return this.permissions.includes(this.getPermissionCode(action, type)) } - public currentUserIsOwner(owner: PaperlessUser): boolean { - return owner?.id === this.currentUser.id + public currentUserOwnsObject(object: ObjectWithPermissions): boolean { + return !object || !object.owner || object.owner.id === this.currentUser.id } public currentUserHasObjectPermissions( action: string, object: ObjectWithPermissions ): boolean { - return (object.permissions[action] as Array)?.includes( - this.currentUser.id + return ( + this.currentUserOwnsObject(object) || + (object.permissions[action]['users'] as Array)?.includes( + this.currentUser.id + ) ) } - public getPermissionCode(permission: PaperlessPermission): string { - return permission.type.replace('%s', permission.action) + public getPermissionCode( + action: PermissionAction, + type: PermissionType + ): string { + return type.replace('%s', action) } public getPermissionKeys(permissionStr: string): { diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 43f3a758e..9ffa29c21 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -28,7 +28,7 @@ from .models import UiSettings from .models import PaperlessTask from .parsers import is_mime_type_supported -from guardian.models import UserObjectPermission +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 @@ -36,6 +36,8 @@ 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 # https://www.django-rest-framework.org/api-guide/serializers/#example @@ -83,14 +85,46 @@ 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): - content_type = ContentType.objects.get_for_model(obj) - user_object_perms = UserObjectPermission.objects.filter( - object_pk=obj.pk, - content_type=content_type, - ).values_list("user", "permission__codename") - return list(user_object_perms) + 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) @@ -111,19 +145,34 @@ class OwnedObjectSerializer(serializers.ModelSerializer): ) return users + def _validate_group_ids(self, group_ids): + groups = Group.objects.none() + if group_ids is not None: + groups = Group.objects.filter(id__in=group_ids) + if not groups.count() == len(group_ids): + raise serializers.ValidationError( + "Some groups in don't exist or were specified twice.", + ) + return groups + def validate_set_permissions(self, set_permissions): - user_dict = { - "view": User.objects.none(), - "change": User.objects.none(), + permissions_dict = { + "view": { + "users": User.objects.none(), + "groups": Group.objects.none(), + }, + "change": { + "users": User.objects.none(), + "groups": Group.objects.none(), + }, } 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 set_permissions: - change_list = set_permissions["change"] - user_dict["change"] = self._validate_user_ids(change_list) - return user_dict + for action in permissions_dict: + users = set_permissions[action]["users"] + permissions_dict[action]["users"] = self._validate_user_ids(users) + groups = set_permissions[action]["groups"] + permissions_dict[action]["groups"] = self._validate_group_ids(groups) + return permissions_dict def __init__(self, *args, **kwargs): self.user = kwargs.pop("user", None) @@ -132,7 +181,8 @@ class OwnedObjectSerializer(serializers.ModelSerializer): def _set_permissions(self, permissions, object): for action in permissions: permission = f"{action}_{object.__class__.__name__.lower()}" - users_to_add = permissions[action] + # users + users_to_add = permissions[action]["users"] users_to_remove = get_users_with_perms( object, only_with_perms_in=[permission], @@ -148,6 +198,23 @@ class OwnedObjectSerializer(serializers.ModelSerializer): 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 create(self, validated_data): if self.user and ( diff --git a/src/documents/tests/test_api.py b/src/documents/tests/test_api.py index e161edfac..5cfc13672 100644 --- a/src/documents/tests/test_api.py +++ b/src/documents/tests/test_api.py @@ -158,7 +158,7 @@ class TestDocumentApi(DirectoriesMixin, APITestCase): response = self.client.get("/api/documents/?fields=", format="json") self.assertEqual(response.status_code, 200) results = response.data["results"] - self.assertEqual(results_full, results) + self.assertEqual(len(results_full[0]), len(results[0])) response = self.client.get("/api/documents/?fields=dgfhs", format="json") self.assertEqual(response.status_code, 200)