frontend permissions dialogs

This commit is contained in:
Michael Shamoon 2022-11-13 22:58:07 -08:00
parent b6f1ced455
commit f461485aa0
13 changed files with 164 additions and 82 deletions

View File

@ -127,7 +127,7 @@ const routes: Routes = [
data: { data: {
requiredPermission: { requiredPermission: {
action: PermissionAction.View, action: PermissionAction.View,
type: PermissionType.Log, type: PermissionType.Admin,
}, },
}, },
}, },

View File

@ -163,7 +163,7 @@
</svg><span>&nbsp;<ng-container i18n>File Tasks<span *ngIf="tasksService.failedFileTasks.length > 0"><span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></span></ng-container></span> </svg><span>&nbsp;<ng-container i18n>File Tasks<span *ngIf="tasksService.failedFileTasks.length > 0"><span class="badge bg-danger ms-2">{{tasksService.failedFileTasks.length}}</span></span></ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item" *ifPermissions="{ action: PermissionAction.View, type: PermissionType.Log }"> <li class="nav-item" *ifPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
<a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> <a class="nav-link" routerLink="logs" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Logs" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor"> <svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#text-left"/> <use xlink:href="assets/bootstrap-icons.svg#text-left"/>

View File

@ -8,7 +8,7 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text> <app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
<app-permissions-select i18n-title title="Permissions" formControlName="permissions"></app-permissions-select> <app-permissions-select i18n-title title="Permissions" formControlName="permissions" [error]="error?.permissions"></app-permissions-select>
</div> </div>
</div> </div>
</div> </div>

View File

@ -26,7 +26,7 @@ export class GroupEditDialogComponent extends EditDialogComponent<PaperlessGroup
getForm(): FormGroup { getForm(): FormGroup {
return new FormGroup({ return new FormGroup({
name: new FormControl(''), name: new FormControl(''),
permissions: new FormControl(''), permissions: new FormControl(null),
}) })
} }
} }

View File

@ -11,19 +11,21 @@
<app-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></app-input-text> <app-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></app-input-text>
<app-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></app-input-text> <app-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></app-input-text>
<div class="form-check form-switch"> <div class="mb-2">
<input type="checkbox" class="form-check-input" id="is_active" formControlName="is_active"> <div class="form-check form-switch form-check-inline">
<label class="form-check-label" for="is_active" i18n>Active</label> <input type="checkbox" class="form-check-input" id="is_active" formControlName="is_active">
<label class="form-check-label" for="is_active" i18n>Active</label>
</div>
<div class="form-check form-switch form-check-inline">
<input type="checkbox" class="form-check-input" id="is_superuser" formControlName="is_superuser">
<label class="form-check-label" for="is_superuser" i18n>Superuser</label>
</div>
</div> </div>
<div class="form-check form-switch"> <app-input-select i18n-title title="Groups" [items]="groups" multiple="true" formControlName="groups"></app-input-select>
<input type="checkbox" class="form-check-input" id="is_superuser" formControlName="is_superuser">
<label class="form-check-label" for="is_superuser" i18n>Superuser</label>
</div>
</div> </div>
<div class="col"> <div class="col">
<app-input-select i18n-title title="Groups" [items]="groups" multiple="true" formControlName="groups"></app-input-select> <app-permissions-select i18n-title title="Permissions" formControlName="user_permissions" [error]="error?.user_permissions"></app-permissions-select>
<app-permissions-select i18n-title title="Permissions" formControlName="permissions"></app-permissions-select>
</div> </div>
</div> </div>
</div> </div>

View File

@ -42,10 +42,10 @@ export class UserEditDialogComponent extends EditDialogComponent<PaperlessUser>
username: new FormControl(''), username: new FormControl(''),
first_name: new FormControl(''), first_name: new FormControl(''),
last_name: new FormControl(''), last_name: new FormControl(''),
is_active: new FormControl(''), is_active: new FormControl(null),
is_superuser: new FormControl(''), is_superuser: new FormControl(null),
groups: new FormControl(''), groups: new FormControl(null),
permissions: new FormControl(''), user_permissions: new FormControl(null),
}) })
} }
} }

View File

@ -1,18 +1,27 @@
<form [formGroup]="form"> <form [formGroup]="form">
<label>{{title}}</label> <label class="form-label">{{title}}</label>
<ul class="list-group"> <ul class="list-group">
<li class="list-group-item" *ngFor="let type of PermissionType | keyvalue" [formGroupName]="type.key"> <li class="list-group-item d-flex">
{{type.key}}: <div class="col-3" i18n>Type</div>
<div class="col" i18n>All</div>
<div class="col" i18n>Add</div>
<div class="col" i18n>Change</div>
<div class="col" i18n>Delete</div>
<div class="col" i18n>View</div>
</li>
<li class="list-group-item d-flex" *ngFor="let type of PermissionType | keyvalue" [formGroupName]="type.key">
<div class="col-3">{{type.key}}:</div>
<div class="form-check form-check-inline form-switch"> <div class="col form-check form-check-inline form-switch">
<input type="checkbox" class="form-check-input" id="{{type.key}}_all" formControlName="all"> <input type="checkbox" class="form-check-input" id="{{type.key}}_all" (change)="toggleAll($event, type.key)" [checked]="typesWithAllActions.has(type.key)">
<label class="form-check-label" for="{{type.key}}_all" i18n>All</label> <label class="form-check-label visually-hidden" for="{{type.key}}_all" i18n>All</label>
</div> </div>
<div *ngFor="let action of PermissionAction | keyvalue" class="form-check form-check-inline" [disabled]="isAll(type.key)"> <div *ngFor="let action of PermissionAction | keyvalue" class="col form-check form-check-inline">
<input type="checkbox" class="form-check-input" id="{{type.key}}_{{action.key}}" formControlName="{{action.key}}"> <input type="checkbox" class="form-check-input" id="{{type.key}}_{{action.key}}" formControlName="{{action.key}}" [attr.disabled]="typesWithAllActions.has(type.key) ? true : null">
<label class="form-check-label" for="{{type.key}}_{{action.key}}" i18n>{{action.key}}</label> <label class="form-check-label visually-hidden" for="{{type.key}}_{{action.key}}" i18n>{{action.key}}</label>
</div> </div>
</li> </li>
<div *ngIf="error" class="invalid-feedback d-block">{{error}}</div>
</ul> </ul>
</form> </form>

View File

@ -10,7 +10,6 @@ import {
PermissionsService, PermissionsService,
PermissionType, PermissionType,
} from 'src/app/services/permissions.service' } from 'src/app/services/permissions.service'
import { AbstractInputComponent } from '../input/abstract-input'
@Component({ @Component({
providers: [ providers: [
@ -33,14 +32,18 @@ export class PermissionsSelectComponent
@Input() @Input()
title: string = 'Permissions' title: string = 'Permissions'
@Input()
error: string
permissions: string[] permissions: string[]
form = new FormGroup({}) form = new FormGroup({})
typesWithAllActions: Set<string> = new Set()
constructor(private readonly permissionsService: PermissionsService) { constructor(private readonly permissionsService: PermissionsService) {
for (const type in PermissionType) { for (const type in PermissionType) {
const control = new FormGroup({}) const control = new FormGroup({})
control.addControl('all', new FormControl(null))
for (const action in PermissionAction) { for (const action in PermissionAction) {
control.addControl(action, new FormControl(null)) control.addControl(action, new FormControl(null))
} }
@ -50,7 +53,7 @@ export class PermissionsSelectComponent
writeValue(permissions: string[]): void { writeValue(permissions: string[]): void {
this.permissions = permissions this.permissions = permissions
this.permissions.forEach((permissionStr) => { this.permissions?.forEach((permissionStr) => {
const { actionKey, typeKey } = const { actionKey, typeKey } =
this.permissionsService.getPermissionKeys(permissionStr) this.permissionsService.getPermissionKeys(permissionStr)
@ -60,20 +63,70 @@ export class PermissionsSelectComponent
} }
} }
}) })
Object.keys(PermissionType).forEach((type) => {
if (Object.values(this.form.get(type).value).every((val) => val)) {
this.typesWithAllActions.add(type)
} else {
this.typesWithAllActions.delete(type)
}
})
} }
onChange = (newValue: string[]) => {}
onTouched = () => {}
disabled: boolean = false
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
throw new Error('Method not implemented.') this.onChange = fn
} }
registerOnTouched(fn: any): void { registerOnTouched(fn: any): void {
throw new Error('Method not implemented.') this.onTouched = fn
} }
setDisabledState?(isDisabled: boolean): void { setDisabledState?(isDisabled: boolean): void {
throw new Error('Method not implemented.') this.disabled = isDisabled
} }
ngOnInit(): void {} ngOnInit(): void {
this.form.valueChanges.subscribe((newValue) => {
let permissions = []
Object.entries(newValue).forEach(([typeKey, typeValue]) => {
// e.g. [Document, { Add: true, View: true ... }]
const selectedActions = Object.entries(typeValue).filter(
([actionKey, actionValue]) => actionValue
)
isAll(key: string): boolean { selectedActions.forEach(([actionKey, actionValue]) => {
return this.form.get(key).get('all').value == true permissions.push(
(PermissionType[typeKey] as string).replace(
'%s',
PermissionAction[actionKey]
)
)
})
if (selectedActions.length == Object.entries(typeValue).length) {
this.typesWithAllActions.add(typeKey)
} else {
this.typesWithAllActions.delete(typeKey)
}
})
this.onChange(permissions)
})
}
toggleAll(event, type) {
const typeGroup = this.form.get(type)
if (event.target.checked) {
Object.keys(PermissionAction).forEach((action) => {
typeGroup.get(action).patchValue(true)
})
this.typesWithAllActions.add(type)
} else {
this.typesWithAllActions.delete(type)
}
} }
} }

View File

@ -245,7 +245,7 @@ export class SettingsComponent
is_active: user.is_active, is_active: user.is_active,
is_superuser: user.is_superuser, is_superuser: user.is_superuser,
groups: user.groups, groups: user.groups,
permissions: user.permissions, user_permissions: user.user_permissions,
} }
this.usersGroup.addControl( this.usersGroup.addControl(
user.id.toString(), user.id.toString(),
@ -257,7 +257,7 @@ export class SettingsComponent
is_active: new FormControl(null), is_active: new FormControl(null),
is_superuser: new FormControl(null), is_superuser: new FormControl(null),
groups: new FormControl(null), groups: new FormControl(null),
permissions: new FormControl(null), user_permissions: new FormControl(null),
}) })
) )
} }
@ -514,7 +514,21 @@ export class SettingsComponent
modal.componentInstance.btnCaption = $localize`Proceed` modal.componentInstance.btnCaption = $localize`Proceed`
modal.componentInstance.confirmClicked.subscribe(() => { modal.componentInstance.confirmClicked.subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled = false
this.usersService.delete(user) this.usersService.delete(user).subscribe({
next: () => {
modal.close()
this.toastService.showInfo($localize`Deleted user`)
this.usersService.listAll().subscribe((r) => {
this.users = r.results
this.initialize()
})
},
error: (e) => {
this.toastService.showError(
$localize`Error deleting user: ${e.toString()}.`
)
},
})
}) })
} }
@ -554,7 +568,21 @@ export class SettingsComponent
modal.componentInstance.btnCaption = $localize`Proceed` modal.componentInstance.btnCaption = $localize`Proceed`
modal.componentInstance.confirmClicked.subscribe(() => { modal.componentInstance.confirmClicked.subscribe(() => {
modal.componentInstance.buttonsEnabled = false modal.componentInstance.buttonsEnabled = false
this.groupsService.delete(group) this.groupsService.delete(group).subscribe({
next: () => {
modal.close()
this.toastService.showInfo($localize`Deleted group`)
this.groupsService.listAll().subscribe((r) => {
this.groups = r.results
this.initialize()
})
},
error: (e) => {
this.toastService.showError(
$localize`Error deleting group: ${e.toString()}.`
)
},
})
}) })
} }
} }

View File

@ -10,6 +10,6 @@ export interface PaperlessUser extends ObjectWithId {
is_active?: boolean is_active?: boolean
is_superuser?: boolean is_superuser?: boolean
groups?: PaperlessGroup[] groups?: PaperlessGroup[]
permissions?: string[] user_permissions?: string[]
inherited_permissions?: string[] inherited_permissions?: string[]
} }

View File

@ -8,20 +8,19 @@ export enum PermissionAction {
} }
export enum PermissionType { export enum PermissionType {
Document = 'documents.%s_document', Document = '%s_document',
Tag = 'documents.%s_tag', Tag = '%s_tag',
Correspondent = 'documents.%s_correspondent', Correspondent = '%s_correspondent',
DocumentType = 'documents.%s_documenttype', DocumentType = '%s_documenttype',
StoragePath = 'documents.%s_storagepath', StoragePath = '%s_storagepath',
SavedView = 'documents.%s_savedview', SavedView = '%s_savedview',
PaperlessTask = 'documents.%s_paperlesstask', PaperlessTask = '%s_paperlesstask',
UISettings = 'documents.%s_uisettings', UISettings = '%s_uisettings',
Comment = 'documents.%s_comment', Comment = '%s_comment',
Log = 'admin.%s_logentry', MailAccount = '%s_mailaccount',
MailAccount = 'paperless_mail.%s_mailaccount', MailRule = '%s_mailrule',
MailRule = 'paperless_mail.%s_mailrule', User = '%s_user',
User = 'auth.%s_user', Admin = '%s_logentry',
Admin = 'admin.%s_logentry',
} }
export interface PaperlessPermission { export interface PaperlessPermission {
@ -51,7 +50,7 @@ export class PermissionsService {
actionKey: string actionKey: string
typeKey: string typeKey: string
} { } {
const matches = permissionStr.match(/\.(.+)_/) const matches = permissionStr.match(/(.+)_/)
let typeKey let typeKey
let actionKey let actionKey
if (matches?.length > 0) { if (matches?.length > 0) {

View File

@ -856,7 +856,7 @@ class UiSettingsView(GenericAPIView):
ui_settings["update_checking"] = { ui_settings["update_checking"] = {
"backend_setting": settings.ENABLE_UPDATE_CHECK, "backend_setting": settings.ENABLE_UPDATE_CHECK,
} }
roles = user.get_all_permissions() roles = user.user_permissions.values_list("codename", flat=True)
return Response( return Response(
{ {
"user_id": user.id, "user_id": user.id,

View File

@ -1,12 +1,21 @@
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User from django.contrib.auth.models import User
from rest_framework import serializers from rest_framework import serializers
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
groups = serializers.SerializerMethodField() groups = serializers.SlugRelatedField(
permissions = serializers.SerializerMethodField() many=True,
queryset=Group.objects.all(),
slug_field="name",
)
user_permissions = serializers.SlugRelatedField(
many=True,
queryset=Permission.objects.all(),
slug_field="codename",
)
inherited_permissions = serializers.SerializerMethodField() inherited_permissions = serializers.SerializerMethodField()
class Meta: class Meta:
@ -21,30 +30,21 @@ class UserSerializer(serializers.ModelSerializer):
"is_active", "is_active",
"is_superuser", "is_superuser",
"groups", "groups",
"permissions", "user_permissions",
"inherited_permissions", "inherited_permissions",
) )
def get_groups(self, obj):
return list(obj.groups.values_list("name", flat=True))
def get_permissions(self, obj):
# obj.get_user_permissions() returns more permissions than desired
permission_natural_keys = []
permissions = obj.user_permissions.all()
for permission in permissions:
permission_natural_keys.append(
permission.natural_key()[1] + "." + permission.natural_key()[0],
)
return permission_natural_keys
def get_inherited_permissions(self, obj): def get_inherited_permissions(self, obj):
return obj.get_group_permissions() return obj.get_group_permissions()
class GroupSerializer(serializers.ModelSerializer): class GroupSerializer(serializers.ModelSerializer):
permissions = serializers.SerializerMethodField() permissions = serializers.SlugRelatedField(
many=True,
queryset=Permission.objects.all(),
slug_field="codename",
)
class Meta: class Meta:
model = Group model = Group
@ -53,12 +53,3 @@ class GroupSerializer(serializers.ModelSerializer):
"name", "name",
"permissions", "permissions",
) )
def get_permissions(self, obj):
permission_natural_keys = []
permissions = obj.permissions.all()
for permission in permissions:
permission_natural_keys.append(
permission.natural_key()[1] + "." + permission.natural_key()[0],
)
return permission_natural_keys