]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Fix: remove admin.logentry perm, use admin (staff) status (#6380)
authorshamoon <4887959+shamoon@users.noreply.github.com>
Sun, 14 Apr 2024 00:35:34 +0000 (17:35 -0700)
committerGitHub <noreply@github.com>
Sun, 14 Apr 2024 00:35:34 +0000 (00:35 +0000)
15 files changed:
docs/usage.md
src-ui/src/app/app-routing.module.ts
src-ui/src/app/components/admin/settings/settings.component.html
src-ui/src/app/components/admin/settings/settings.component.spec.ts
src-ui/src/app/components/admin/settings/settings.component.ts
src-ui/src/app/components/app-frame/app-frame.component.html
src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html
src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts
src-ui/src/app/guards/permissions.guard.ts
src-ui/src/app/services/permissions.service.spec.ts
src-ui/src/app/services/permissions.service.ts
src/documents/permissions.py
src/documents/tests/test_api_permissions.py
src/documents/tests/test_api_uisettings.py
src/documents/views.py

index d77b3b2a6ff503c1f438c2d9d8c34dd022e67fbb..7cedb976a998ae64bccf90e92ddadcf6fd7cd6d0 100644 (file)
@@ -241,6 +241,11 @@ permissions can be granted to limit access to certain parts of the UI (and corre
 
     Superusers can access all parts of the front and backend application as well as any and all objects.
 
+#### Admin Status
+
+Admin status (Django 'staff status') grants access to viewing the paperless logs and the system status dialog
+as well as accessing the Django backend.
+
 #### Detailed Explanation of Global Permissions {#global-permissions}
 
 Global permissions define what areas of the app and API endpoints the user can access. For example, they
@@ -249,7 +254,6 @@ still have "object-level" permissions.
 
 | Type          | Details                                                                                                                                                                                             |
 | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| Admin         | _View_ or higher permissions grants access to the logs view as well as the system status.                                                                                                           |
 | AppConfig     | _Change_ or higher permissions grants access to the "Application Configuration" area.                                                                                                               |
 | Correspondent | Grants global permissions to add, edit, delete or view Correspondents.                                                                                                                              |
 | CustomField   | Grants global permissions to add, edit, delete or view Custom Fields.                                                                                                                               |
index 3eebd31bd2001ee967dff9ac2b7c13f9af949f64..12b412f67bb38c27ce364053a09e8c5e88167911 100644 (file)
@@ -141,10 +141,7 @@ export const routes: Routes = [
         component: LogsComponent,
         canActivate: [PermissionsGuard],
         data: {
-          requiredPermission: {
-            action: PermissionAction.View,
-            type: PermissionType.Admin,
-          },
+          requireAdmin: true,
         },
       },
       // redirect old paths
index 42147a9b83cc769b91bfa9f97b117e7a9a80fb11..0fc744edb544e7a9df5f22b3a478b16883a6a998 100644 (file)
@@ -7,29 +7,30 @@
   <button class="btn btn-sm btn-outline-primary" (click)="tourService.start()">
     <i-bs class="me-1" name="airplane"></i-bs>&nbsp;<ng-container i18n>Start tour</ng-container>
   </button>
-  <button class="btn btn-sm btn-outline-primary position-relative ms-md-5 me-1" (click)="showSystemStatus()"
-    [disabled]="!systemStatus"
-    *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
-    @if (!systemStatus) {
-      <div class="spinner-border spinner-border-sm me-1 h-75" role="status"></div>
-    } @else {
-      <i-bs class="me-2" name="card-checklist"></i-bs>
-      @if (systemStatusHasErrors) {
-        <span class="badge bg-body position-absolute top-0 start-100 translate-middle rounded-pill p-0">
-          <i-bs name="exclamation-circle-fill" class="text-danger" width="1.75em" height="1.75em"></i-bs>
-        </span>
+  @if (permissionsService.isAdmin()) {
+    <button class="btn btn-sm btn-outline-primary position-relative ms-md-5 me-1" (click)="showSystemStatus()"
+      [disabled]="!systemStatus">
+      @if (!systemStatus) {
+        <div class="spinner-border spinner-border-sm me-1 h-75" role="status"></div>
       } @else {
-        <span class="badge bg-body position-absolute top-0 start-100 translate-middle rounded-pill p-0">
-          <i-bs name="check-circle-fill" class="text-primary" width="1.75em" height="1.75em"></i-bs>
-        </span>
+        <i-bs class="me-2" name="card-checklist"></i-bs>
+        @if (systemStatusHasErrors) {
+          <span class="badge bg-body position-absolute top-0 start-100 translate-middle rounded-pill p-0">
+            <i-bs name="exclamation-circle-fill" class="text-danger" width="1.75em" height="1.75em"></i-bs>
+          </span>
+        } @else {
+          <span class="badge bg-body position-absolute top-0 start-100 translate-middle rounded-pill p-0">
+            <i-bs name="check-circle-fill" class="text-primary" width="1.75em" height="1.75em"></i-bs>
+          </span>
+        }
       }
-    }
-    <ng-container i18n>System Status</ng-container>
-  </button>
-  <a *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }" class="btn btn-sm btn-primary" href="admin/" target="_blank">
-    <ng-container i18n>Open Django Admin</ng-container>
-    &nbsp;<i-bs name="arrow-up-right"></i-bs>
-  </a>
+      <ng-container i18n>System Status</ng-container>
+    </button>
+    <a class="btn btn-sm btn-primary" href="admin/" target="_blank">
+      <ng-container i18n>Open Django Admin</ng-container>
+      &nbsp;<i-bs name="arrow-up-right"></i-bs>
+    </a>
+  }
 </pngx-page-header>
 
 <form [formGroup]="settingsForm" (ngSubmit)="saveSettings()">
index 6110f7d1d8dde6f2c2e487f7770440f7f9caf427..d53f57b6985544e33ab94aeb89d139ccdf7db7cb 100644 (file)
@@ -418,6 +418,7 @@ describe('SettingsComponent', () => {
       },
     }
     jest.spyOn(systemStatusService, 'get').mockReturnValue(of(status))
+    jest.spyOn(permissionsService, 'isAdmin').mockReturnValue(true)
     completeSetup()
     expect(component['systemStatus']).toEqual(status) // private
     expect(component.systemStatusHasErrors).toBeTruthy()
index f04af2f9db307f90002b65450e621c1f25076eb5..33f6949a149cfca030d8cd4202adab5f0aba6a38 100644 (file)
@@ -121,7 +121,7 @@ export class SettingsComponent
   users: User[]
   groups: Group[]
 
-  private systemStatus: SystemStatus
+  public systemStatus: SystemStatus
 
   get systemStatusHasErrors(): boolean {
     return (
@@ -385,12 +385,7 @@ export class SettingsComponent
       this.settingsForm.patchValue(currentFormValue)
     }
 
-    if (
-      this.permissionsService.currentUserCan(
-        PermissionAction.View,
-        PermissionType.Admin
-      )
-    ) {
+    if (this.permissionsService.isAdmin()) {
       this.systemStatusService.get().subscribe((status) => {
         this.systemStatus = status
       })
index b79f99cc04a72cdd35f509a213110bb54c700966..bdc8d08f2596334d83df099404ab0bb01288330c 100644 (file)
                 }
               </a>
             </li>
-            <li class="nav-item app-link" *pngxIfPermissions="{ 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">
-                <i-bs class="me-1" name="text-left"></i-bs><span>&nbsp;<ng-container i18n>Logs</ng-container></span>
-              </a>
-            </li>
+            @if (permissionsService.isAdmin()) {
+              <li class="nav-item app-link">
+                <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">
+                  <i-bs class="me-1" name="text-left"></i-bs><span>&nbsp;<ng-container i18n>Logs</ng-container></span>
+                </a>
+              </li>
+            }
             <li class="nav-item mt-2" tourAnchor="tour.outro">
               <a class="px-3 py-2 text-muted small d-flex align-items-center flex-wrap text-decoration-none"
                 target="_blank" rel="noopener noreferrer" href="https://docs.paperless-ngx.com" ngbPopover="Documentation"
index 25e7fb964511b9bf11160cd25b3044417d929c37..ca834a3ade5104fe05900e421b6a1f88c476a964 100644 (file)
           <pngx-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></pngx-input-text>
           <pngx-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></pngx-input-text>
 
-          <div class="mb-2">
+          <div class="mb-2 d-flex flex-column">
             <div class="form-check form-switch form-check-inline">
               <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_staff" formControlName="is_staff">
+              <label class="form-check-label" for="is_staff"><ng-container i18n>Admin</ng-container> <small class="form-text text-muted ms-1" i18n>Access logs, Django backend</small></label>
+            </div>
             <div class="form-check form-switch form-check-inline">
               <input type="checkbox" class="form-check-input" id="is_superuser" formControlName="is_superuser" (change)="onToggleSuperUser()">
               <label class="form-check-label" for="is_superuser"><ng-container i18n>Superuser</ng-container> <small class="form-text text-muted ms-1" i18n>(Grants all permissions and can view objects)</small></label>
index ebbf0766e2b3ed0eb015532021ef99bfc7216d26..baadfa54178a370636cfe71f1642eed936474551 100644 (file)
@@ -56,6 +56,7 @@ export class UserEditDialogComponent
       first_name: new FormControl(''),
       last_name: new FormControl(''),
       is_active: new FormControl(true),
+      is_staff: new FormControl(true),
       is_superuser: new FormControl(false),
       groups: new FormControl([]),
       user_permissions: new FormControl([]),
index 77bf6071ae47892348881e2cba70168c9c42c677..402cc00f5a371bb530fb2dfb91e69201b29b4da0 100644 (file)
@@ -23,10 +23,12 @@ export class PermissionsGuard {
     state: RouterStateSnapshot
   ): boolean | UrlTree {
     if (
-      !this.permissionsService.currentUserCan(
-        route.data.requiredPermission.action,
-        route.data.requiredPermission.type
-      )
+      (route.data.requireAdmin && !this.permissionsService.isAdmin()) ||
+      (route.data.requiredPermission &&
+        !this.permissionsService.currentUserCan(
+          route.data.requiredPermission.action,
+          route.data.requiredPermission.type
+        ))
     ) {
       // Check if tour is running 1 = TourState.ON
       if (this.tourService.getStatus() !== 1) {
index 61e7c9978fdddba49d8234b927a97ed89804cba4..f4e01945e36eecf68420b5ede4984e60590dc0aa 100644 (file)
@@ -418,4 +418,25 @@ describe('PermissionsService', () => {
       )
     ).toBeTruthy()
   })
+
+  it('correctly checks admin status', () => {
+    permissionsService.initialize([], {
+      username: 'testuser',
+      last_name: 'User',
+      first_name: 'Test',
+      id: 1,
+      is_staff: true,
+    })
+
+    expect(permissionsService.isAdmin()).toBeTruthy()
+
+    permissionsService.initialize([], {
+      username: 'testuser',
+      last_name: 'User',
+      first_name: 'Test',
+      id: 1,
+    })
+
+    expect(permissionsService.isAdmin()).toBeFalsy()
+  })
 })
index 29b0c1a225a1bd2c5dd4bc318e5d19392b33fae8..0648f461f53c8e17dd3112aa34239edae8ed8380 100644 (file)
@@ -24,7 +24,6 @@ export enum PermissionType {
   MailRule = '%s_mailrule',
   User = '%s_user',
   Group = '%s_group',
-  Admin = '%s_logentry',
   ShareLink = '%s_sharelink',
   CustomField = '%s_customfield',
   Workflow = '%s_workflow',
@@ -52,6 +51,10 @@ export class PermissionsService {
     )
   }
 
+  public isAdmin(): boolean {
+    return this.currentUser?.is_staff
+  }
+
   public currentUserOwnsObject(object: ObjectWithPermissions): boolean {
     return (
       !object ||
index bdd6fd55518115cad65312a7f6625acf24a6e360..d16d7aa1c7ce2b25614742529585cf9144182d00 100644 (file)
@@ -40,7 +40,7 @@ class PaperlessObjectPermissions(DjangoObjectPermissions):
 
 class PaperlessAdminPermissions(BasePermission):
     def has_permission(self, request, view):
-        return request.user.has_perm("admin.view_logentry")
+        return request.user.is_staff
 
 
 def get_groups_with_only_permission(obj, codename):
index 92e47a1eda86c1152d60b5412393be12ff68d1e2..d7131b834015a36c1c57a11bd4763a74dc160438 100644 (file)
@@ -131,6 +131,7 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
     def test_api_sufficient_permissions(self):
         user = User.objects.create_user(username="test")
         user.user_permissions.add(*Permission.objects.all())
+        user.is_staff = True
         self.client.force_authenticate(user)
 
         Document.objects.create(title="Test")
index cb85de91ad73b72e80c8478e94499bd743353063..2cb6af6f21ab50dcd0c2af6a54c121f6d31489a4 100644 (file)
@@ -27,6 +27,7 @@ class TestApiUiSettings(DirectoriesMixin, APITestCase):
             {
                 "id": self.test_user.id,
                 "username": self.test_user.username,
+                "is_staff": True,
                 "is_superuser": True,
                 "groups": [],
                 "first_name": self.test_user.first_name,
index 655108f0569492fb000e6f20d536654fae66f342..5841649d087e76a0d852570953fe12bac0125752 100644 (file)
@@ -1270,6 +1270,7 @@ class UiSettingsView(GenericAPIView):
         user_resp = {
             "id": user.id,
             "username": user.username,
+            "is_staff": user.is_staff,
             "is_superuser": user.is_superuser,
             "groups": list(user.groups.values_list("id", flat=True)),
         }