mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-02 13:45:10 -05: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.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):
|
||||
|
@ -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