]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Increase usability of role management by enabling the user to check... 23960/head
authorVolker Theile <vtheile@suse.com>
Fri, 7 Sep 2018 09:14:38 +0000 (11:14 +0200)
committerVolker Theile <vtheile@suse.com>
Thu, 20 Sep 2018 13:35:16 +0000 (15:35 +0200)
Fixes: https://tracker.ceph.com/issues/35695
Signed-off-by: Volker Theile <vtheile@suse.com>
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-details/role-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.scss
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.ts

index a91ff0061bea6b1f4ded151dd760777c376383a8..7389a48f83088ab090af3aa702ada0feb35e11e0 100644 (file)
@@ -1,32 +1,13 @@
 <tabset  *ngIf="selection?.hasSingleSelection">
   <tab heading="Details" i18n-heading>
-    <table class="table table-bordered table-hover">
-      <thead>
-      <tr>
-        <th></th>
-        <th class="text-center">Read</th>
-        <th class="text-center">Create</th>
-        <th class="text-center">Update</th>
-        <th class="text-center">Delete</th>
-      </tr>
-      </thead>
-      <tbody>
-      <tr *ngFor="let scope of scopes">
-        <td i18n
-            class="bold col-sm-3">
-          {{ scope }}
-        </td>
-        <td class="col-sm-2 text-center"
-            *ngFor="let column of ['read', 'create', 'update', 'delete']">
-          <span *ngIf="selectedItem.scopes_permissions[scope] && selectedItem.scopes_permissions[scope].indexOf(column) !== -1">
-            <i class="fa fa-check-square-o" aria-hidden="true"></i>
-          </span>
-          <span *ngIf="!selectedItem.scopes_permissions[scope] || selectedItem.scopes_permissions[scope].indexOf(column) === -1">
-            <i class="fa fa-square-o" aria-hidden="true"></i>
-          </span>
-        </td>
-      </tr>
-      </tbody>
-    </table>
+    <cd-table [data]="scopes_permissions"
+              [columns]="columns"
+              columnMode="flex"
+              [toolHeader]="false"
+              [autoReload]="false"
+              [autoSave]="false"
+              [footer]="false"
+              [limit]="0">
+    </cd-table>
   </tab>
 </tabset>
index f0234b33b4719cbf4d8e50694dcffbe441ddf43a..3d34a3af5b2abd56a04f72e231053ce7095aa88f 100644 (file)
@@ -5,6 +5,7 @@ import { RouterTestingModule } from '@angular/router/testing';
 import { ToastModule } from 'ng2-toastr';
 import { TabsModule } from 'ngx-bootstrap';
 
+import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 import { SharedModule } from '../../../shared/shared.module';
 import { RoleDetailsComponent } from './role-details.component';
 
@@ -34,4 +35,50 @@ describe('RoleDetailsComponent', () => {
   it('should create', () => {
     expect(component).toBeTruthy();
   });
+
+  it('should create scopes permissions [1/2]', () => {
+    component.scopes = ['log', 'rgw'];
+    component.selection = new CdTableSelection();
+    component.selection.selected = [
+      {
+        description: 'RGW Manager',
+        name: 'rgw-manager',
+        scopes_permissions: {
+          rgw: ['read', 'create', 'update', 'delete']
+        },
+        system: true
+      }
+    ];
+    component.selection.update();
+    expect(component.scopes_permissions.length).toBe(0);
+    component.ngOnChanges();
+    expect(component.scopes_permissions).toEqual([
+      { scope: 'log', read: false, create: false, update: false, delete: false },
+      { scope: 'rgw', read: true, create: true, update: true, delete: true }
+    ]);
+  });
+
+  it('should create scopes permissions [2/2]', () => {
+    component.scopes = ['cephfs', 'log', 'rgw'];
+    component.selection = new CdTableSelection();
+    component.selection.selected = [
+      {
+        description: 'Test',
+        name: 'test',
+        scopes_permissions: {
+          log: ['read', 'update'],
+          rgw: ['read', 'create', 'update']
+        },
+        system: false
+      }
+    ];
+    component.selection.update();
+    expect(component.scopes_permissions.length).toBe(0);
+    component.ngOnChanges();
+    expect(component.scopes_permissions).toEqual([
+      { scope: 'cephfs', read: false, create: false, update: false, delete: false },
+      { scope: 'log', read: true, create: false, update: true, delete: false },
+      { scope: 'rgw', read: true, create: true, update: true, delete: false }
+    ]);
+  });
 });
index 5f5b27ce0725fae66596d68d47daf6bd10438605..a81f4cbda1cf7389b272395f6b61ad4762d61f82 100644 (file)
@@ -1,5 +1,9 @@
-import { Component, Input, OnChanges } from '@angular/core';
+import { Component, Input, OnChanges, OnInit } from '@angular/core';
 
+import * as _ from 'lodash';
+
+import { CellTemplate } from '../../../shared/enum/cell-template.enum';
+import { CdTableColumn } from '../../../shared/models/cd-table-column';
 import { CdTableSelection } from '../../../shared/models/cd-table-selection';
 
 @Component({
@@ -7,18 +11,72 @@ import { CdTableSelection } from '../../../shared/models/cd-table-selection';
   templateUrl: './role-details.component.html',
   styleUrls: ['./role-details.component.scss']
 })
-export class RoleDetailsComponent implements OnChanges {
+export class RoleDetailsComponent implements OnChanges, OnInit {
   @Input()
   selection: CdTableSelection;
   @Input()
   scopes: Array<string>;
   selectedItem: any;
 
+  columns: CdTableColumn[];
+  scopes_permissions: Array<any> = [];
+
   constructor() {}
 
+  ngOnInit() {
+    this.columns = [
+      {
+        prop: 'scope',
+        name: 'Scope',
+        flexGrow: 2
+      },
+      {
+        prop: 'read',
+        name: 'Read',
+        flexGrow: 1,
+        cellClass: 'text-center',
+        cellTransformation: CellTemplate.checkIcon
+      },
+      {
+        prop: 'create',
+        name: 'Create',
+        flexGrow: 1,
+        cellClass: 'text-center',
+        cellTransformation: CellTemplate.checkIcon
+      },
+      {
+        prop: 'update',
+        name: 'Update',
+        flexGrow: 1,
+        cellClass: 'text-center',
+        cellTransformation: CellTemplate.checkIcon
+      },
+      {
+        prop: 'delete',
+        name: 'Delete',
+        flexGrow: 1,
+        cellClass: 'text-center',
+        cellTransformation: CellTemplate.checkIcon
+      }
+    ];
+  }
+
   ngOnChanges() {
     if (this.selection.hasSelection) {
       this.selectedItem = this.selection.first();
+      // Build the scopes/permissions data used by the data table.
+      const scopes_permissions = [];
+      _.each(this.scopes, (scope) => {
+        const scope_permission = { read: false, create: false, update: false, delete: false };
+        scope_permission['scope'] = scope;
+        if (scope in this.selectedItem['scopes_permissions']) {
+          _.each(this.selectedItem['scopes_permissions'][scope], (permission) => {
+            scope_permission[permission] = true;
+          });
+        }
+        scopes_permissions.push(scope_permission);
+      });
+      this.scopes_permissions = scopes_permissions;
     }
   }
 }
index 2366aec323740e225699ed91c3d3a5c755972a4a..a35a361d78b3bd802814ade3d6c7951f1c9a4eef 100644 (file)
@@ -48,7 +48,7 @@
              [ngClass]="{'has-error': roleForm.showError('description', formDir)}">
           <label i18n
                  class="control-label col-sm-3"
-                 for="name">Description
+                 for="description">Description
           </label>
           <div class="col-sm-9">
             <input class="form-control"
         <!-- Permissions -->
         <div class="form-group">
           <label i18n
-                 class="control-label col-sm-3"
-                 for="name">Permissions
+                 class="control-label col-sm-3">Permissions
           </label>
           <div class="col-sm-9">
-            <table class="table table-bordered table-hover">
-              <thead>
-              <tr>
-                <th></th>
-                <th class="text-center">Read</th>
-                <th class="text-center">Create</th>
-                <th class="text-center">Update</th>
-                <th class="text-center">Delete</th>
-              </tr>
-              </thead>
-              <tbody>
-              <tr *ngFor="let scope of scopes">
-                <td i18n
-                    class="bold col-sm-3">
-                  {{ scope }}
-                </td>
-                <td class="col-sm-2 text-center clickable"
-                    *ngFor="let column of ['read', 'create', 'update', 'delete']">
-                  <div class="checkbox checkbox-primary">
-                    <input type="checkbox"
-                           [checked]="roleForm.getValue('scopes_permissions')[scope] && roleForm.getValue('scopes_permissions')[scope].indexOf(column) !== -1"
-                           (change)="hadlePermissionClick(scope, column)">
-                    <label></label>
-                  </div>
-                </td>
-              </tr>
-              </tbody>
-            </table>
+            <cd-table [data]="scopes_permissions"
+                      [columns]="columns"
+                      columnMode="flex"
+                      [toolHeader]="false"
+                      [autoReload]="false"
+                      [autoSave]="false"
+                      [footer]="false"
+                      [limit]="0">
+            </cd-table>
           </div>
         </div>
 
           <button i18n
                   type="button"
                   class="btn btn-sm btn-default"
-                  routerLink="/user-management/users/roles">
+                  routerLink="/user-management/roles">
             Back
           </button>
         </div>
     </div>
   </form>
 </div>
+
+<ng-template #cellScopeCheckboxTpl
+             let-column="column"
+             let-row="row"
+             let-value="value">
+  <div class="checkbox checkbox-primary">
+    <input id="scope_{{ row.scope }}"
+           type="checkbox"
+           [checked]="isRowChecked(row.scope)"
+           (change)="onClickCellCheckbox(row.scope, column.prop, $event)">
+    <label class="datatable-permissions-scope-cell-label"
+           for="scope_{{ row.scope }}">{{ value }}</label>
+  </div>
+</ng-template>
+
+<ng-template #cellPermissionCheckboxTpl
+             let-column="column"
+             let-row="row"
+             let-value="value">
+  <div class="checkbox checkbox-primary">
+    <input type="checkbox"
+           [checked]="value"
+           (change)="onClickCellCheckbox(row.scope, column.prop, $event)">
+    <label></label>
+  </div>
+</ng-template>
+
+<ng-template #headerPermissionCheckboxTpl
+             let-column="column">
+  <div class="checkbox checkbox-primary">
+    <input id="header_{{ column.prop }}"
+           type="checkbox"
+           [checked]="isHeaderChecked(column.prop)"
+           (change)="onClickHeaderCheckbox(column.prop, $event)">
+    <label class="datatable-permissions-header-cell-label"
+           for="header_{{ column.prop }}">{{ column.name }}</label>
+  </div>
+</ng-template>
index 564487d0e7633449eb56d9e25cbb7a7ddaf6227c..3caafa2ee6249155d9f2b86a2a4080a137f5fc2a 100644 (file)
@@ -1,5 +1,4 @@
-@import '../../../../defaults';
-
-thead {
-  background-color: $color-table-header-bg;
+.datatable-permissions-header-cell-label,
+.datatable-permissions-scope-cell-label {
+  font-weight: bold;
 }
index 6da5dee4361e0768e4c92b7805ee733573a92413..e5d1c0399980ef1b4c459854977a1b58b85952c8 100644 (file)
@@ -101,6 +101,63 @@ describe('RoleFormComponent', () => {
       roleReq.flush({});
       expect(router.navigate).toHaveBeenCalledWith(['/user-management/roles']);
     });
+
+    it('should check all perms for a scope', () => {
+      form.get('scopes_permissions').setValue({ cephfs: ['read'] });
+      component.onClickCellCheckbox('grafana', 'scope');
+      const scopes_permissions = form.getValue('scopes_permissions');
+      expect(Object.keys(scopes_permissions)).toContain('grafana');
+      expect(scopes_permissions['grafana']).toEqual(['create', 'delete', 'read', 'update']);
+    });
+
+    it('should uncheck all perms for a scope', () => {
+      form.get('scopes_permissions').setValue({ cephfs: ['read', 'create', 'update', 'delete'] });
+      component.onClickCellCheckbox('cephfs', 'scope');
+      const scopes_permissions = form.getValue('scopes_permissions');
+      expect(Object.keys(scopes_permissions)).not.toContain('cephfs');
+    });
+
+    it('should uncheck all scopes and perms', () => {
+      component.scopes = ['cephfs', 'grafana'];
+      form.get('scopes_permissions').setValue({ cephfs: ['read', 'delete'], grafana: ['update'] });
+      component.onClickHeaderCheckbox('scope', { target: { checked: false } });
+      const scopes_permissions = form.getValue('scopes_permissions');
+      expect(scopes_permissions).toEqual({});
+    });
+
+    it('should check all scopes and perms', () => {
+      component.scopes = ['cephfs', 'grafana'];
+      form
+        .get('scopes_permissions')
+        .setValue({ cephfs: ['create', 'update'], grafana: ['delete'] });
+      component.onClickHeaderCheckbox('scope', { target: { checked: true } });
+      const scopes_permissions = form.getValue('scopes_permissions');
+      const keys = Object.keys(scopes_permissions);
+      expect(keys).toEqual(['cephfs', 'grafana']);
+      keys.forEach((key) => {
+        expect(scopes_permissions[key].sort()).toEqual(['create', 'delete', 'read', 'update']);
+      });
+    });
+
+    it('should check if column is checked', () => {
+      component.scopes_permissions = [
+        { scope: 'a', read: true, create: true, update: true, delete: true },
+        { scope: 'b', read: false, create: true, update: false, delete: true }
+      ];
+      expect(component.isRowChecked('a')).toBeTruthy();
+      expect(component.isRowChecked('b')).toBeFalsy();
+      expect(component.isRowChecked('c')).toBeFalsy();
+    });
+
+    it('should check if header is checked', () => {
+      component.scopes_permissions = [
+        { scope: 'a', read: true, create: true, update: false, delete: true },
+        { scope: 'b', read: false, create: true, update: false, delete: true }
+      ];
+      expect(component.isHeaderChecked('read')).toBeFalsy();
+      expect(component.isHeaderChecked('create')).toBeTruthy();
+      expect(component.isHeaderChecked('update')).toBeFalsy();
+    });
   });
 
   describe('edit mode', () => {
@@ -117,7 +174,6 @@ describe('RoleFormComponent', () => {
       component.ngOnInit();
       const reqScopes = httpTesting.expectOne('ui-api/scope');
       expect(reqScopes.request.method).toBe('GET');
-      reqScopes.flush(scopes);
     });
 
     afterEach(() => {
@@ -142,10 +198,10 @@ describe('RoleFormComponent', () => {
     });
 
     it('should submit', () => {
+      component.onClickCellCheckbox('osd', 'update');
+      component.onClickCellCheckbox('osd', 'create');
+      component.onClickCellCheckbox('user', 'read');
       component.submit();
-      component.hadlePermissionClick('osd', 'update');
-      component.hadlePermissionClick('osd', 'create');
-      component.hadlePermissionClick('user', 'read');
       const roleReq = httpTesting.expectOne(`api/role/${role.name}`);
       expect(roleReq.request.method).toBe('PUT');
       expect(roleReq.request.body).toEqual({
index a3e544f8b7810a69210399454e66115adaa94576..a25502bdef1597b3b7b670b2bbcdc4e93f8fe3e4 100644 (file)
@@ -1,14 +1,17 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
 import { FormControl, Validators } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 
+import * as _ from 'lodash';
 import { BsModalRef } from 'ngx-bootstrap';
+import { forkJoin as observableForkJoin } from 'rxjs';
 
 import { RoleService } from '../../../shared/api/role.service';
 import { ScopeService } from '../../../shared/api/scope.service';
 import { NotificationType } from '../../../shared/enum/notification-type.enum';
 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
 import { CdValidators } from '../../../shared/forms/cd-validators';
+import { CdTableColumn } from '../../../shared/models/cd-table-column';
 import { NotificationService } from '../../../shared/services/notification.service';
 import { RoleFormMode } from './role-form-mode.enum';
 import { RoleFormModel } from './role-form.model';
@@ -19,11 +22,21 @@ import { RoleFormModel } from './role-form.model';
   styleUrls: ['./role-form.component.scss']
 })
 export class RoleFormComponent implements OnInit {
+  @ViewChild('headerPermissionCheckboxTpl')
+  headerPermissionCheckboxTpl: TemplateRef<any>;
+  @ViewChild('cellScopeCheckboxTpl')
+  cellScopeCheckboxTpl: TemplateRef<any>;
+  @ViewChild('cellPermissionCheckboxTpl')
+  cellPermissionCheckboxTpl: TemplateRef<any>;
+
   modalRef: BsModalRef;
 
   roleForm: CdFormGroup;
   response: RoleFormModel;
-  scopes: Array<string>;
+
+  columns: CdTableColumn[];
+  scopes: Array<string> = [];
+  scopes_permissions: Array<any> = [];
 
   roleFormMode = RoleFormMode;
   mode: RoleFormMode;
@@ -36,6 +49,7 @@ export class RoleFormComponent implements OnInit {
     private notificationService: NotificationService
   ) {
     this.createForm();
+    this.listenToChanges();
   }
 
   createForm() {
@@ -50,48 +64,193 @@ export class RoleFormComponent implements OnInit {
   }
 
   ngOnInit() {
+    this.columns = [
+      {
+        prop: 'scope',
+        name: 'All',
+        flexGrow: 2,
+        cellTemplate: this.cellScopeCheckboxTpl,
+        headerTemplate: this.headerPermissionCheckboxTpl
+      },
+      {
+        prop: 'read',
+        name: 'Read',
+        flexGrow: 1,
+        cellClass: 'text-center',
+        cellTemplate: this.cellPermissionCheckboxTpl,
+        headerTemplate: this.headerPermissionCheckboxTpl
+      },
+      {
+        prop: 'create',
+        name: 'Create',
+        flexGrow: 1,
+        cellClass: 'text-center',
+        cellTemplate: this.cellPermissionCheckboxTpl,
+        headerTemplate: this.headerPermissionCheckboxTpl
+      },
+      {
+        prop: 'update',
+        name: 'Update',
+        flexGrow: 1,
+        cellClass: 'text-center',
+        cellTemplate: this.cellPermissionCheckboxTpl,
+        headerTemplate: this.headerPermissionCheckboxTpl
+      },
+      {
+        prop: 'delete',
+        name: 'Delete',
+        flexGrow: 1,
+        cellClass: 'text-center',
+        cellTemplate: this.cellPermissionCheckboxTpl,
+        headerTemplate: this.headerPermissionCheckboxTpl
+      }
+    ];
     if (this.router.url.startsWith('/user-management/roles/edit')) {
       this.mode = this.roleFormMode.editing;
     }
-    this.scopeService.list().subscribe((scopes: Array<string>) => {
-      this.scopes = scopes;
-    });
     if (this.mode === this.roleFormMode.editing) {
       this.initEdit();
+    } else {
+      this.initCreate();
     }
   }
 
+  initCreate() {
+    // Load the scopes and initialize the default scopes/permissions data.
+    this.scopeService.list().subscribe((scopes: Array<string>) => {
+      this.scopes = scopes;
+      this.roleForm.get('scopes_permissions').setValue({});
+    });
+  }
+
   initEdit() {
-    this.disableForEdit();
+    // Disable the 'Name' input field.
+    this.roleForm.get('name').disable();
+    // Load the scopes and the role data.
     this.route.params.subscribe((params: { name: string }) => {
-      const name = params.name;
-      this.roleService.get(name).subscribe((roleFormModel: RoleFormModel) => {
-        this.setResponse(roleFormModel);
+      const observables = [];
+      observables.push(this.scopeService.list());
+      observables.push(this.roleService.get(params.name));
+      observableForkJoin(observables).subscribe((resp: any[]) => {
+        this.scopes = resp[0];
+        ['name', 'description', 'scopes_permissions'].forEach((key) =>
+          this.roleForm.get(key).setValue(resp[1][key])
+        );
       });
     });
   }
 
-  disableForEdit() {
-    this.roleForm.get('name').disable();
+  listenToChanges() {
+    // Create/Update the data which is used by the data table to display the
+    // scopes/permissions every time the form field value has been changed.
+    this.roleForm.get('scopes_permissions').valueChanges.subscribe((value) => {
+      const scopes_permissions = [];
+      _.each(this.scopes, (scope) => {
+        // Set the defaults values.
+        const scope_permission = { read: false, create: false, update: false, delete: false };
+        scope_permission['scope'] = scope;
+        // Apply settings from the given value if they exist.
+        if (scope in value) {
+          _.each(value[scope], (permission) => {
+            scope_permission[permission] = true;
+          });
+        }
+        scopes_permissions.push(scope_permission);
+      });
+      this.scopes_permissions = scopes_permissions;
+    });
   }
 
-  setResponse(response: RoleFormModel) {
-    ['name', 'description', 'scopes_permissions'].forEach((key) =>
-      this.roleForm.get(key).setValue(response[key])
+  /**
+   * Checks if the specified row checkbox needs to be rendered as checked.
+   * @param {string} scope The scope to be checked, e.g. 'cephfs', 'grafana',
+   *   'osd', 'pool' ...
+   * @return Returns true if all permissions (read, create, update, delete)
+   *   are checked for the specified scope, otherwise false.
+   */
+  isRowChecked(scope: string) {
+    const scope_permission = _.find(this.scopes_permissions, (o) => {
+      return o['scope'] === scope;
+    });
+    if (_.isUndefined(scope_permission)) {
+      return false;
+    }
+    return (
+      scope_permission['read'] &&
+      scope_permission['create'] &&
+      scope_permission['update'] &&
+      scope_permission['delete']
     );
   }
 
-  hadlePermissionClick(scope: string, permission: string) {
-    const permissions = this.roleForm.getValue('scopes_permissions');
-    if (!permissions[scope]) {
-      permissions[scope] = [];
+  /**
+   * Checks if the specified header checkbox needs to be rendered as checked.
+   * @param {string} property The property/permission (read, create,
+   *   update, delete) to be checked. If 'scope' is given, all permissions
+   *   are checked.
+   * @return Returns true if specified property/permission is selected
+   *   for all scopes, otherwise false.
+   */
+  isHeaderChecked(property: string) {
+    let permissions = [property];
+    if ('scope' === property) {
+      permissions = ['read', 'create', 'update', 'delete'];
     }
-    const index = permissions[scope].indexOf(permission);
-    if (index === -1) {
-      permissions[scope].push(permission);
+    return permissions.every((permission) => {
+      return this.scopes_permissions.every((scope_permission) => {
+        return scope_permission[permission];
+      });
+    });
+  }
+
+  onClickCellCheckbox(scope: string, property: string, event: Event = null) {
+    // Use a copy of the form field data to do not trigger the redrawing of the
+    // data table with every change.
+    const scopes_permissions = _.cloneDeep(this.roleForm.getValue('scopes_permissions'));
+    let permissions = [property];
+    if ('scope' === property) {
+      permissions = ['read', 'create', 'update', 'delete'];
+    }
+    if (!(scope in scopes_permissions)) {
+      scopes_permissions[scope] = [];
+    }
+    // Add or remove the given permission(s) depending on the click event or if no
+    // click event is given then add/remove them if they are absent/exist.
+    if (
+      (event && event.target['checked']) ||
+      !_.isEqual(permissions.sort(), _.intersection(scopes_permissions[scope], permissions).sort())
+    ) {
+      scopes_permissions[scope] = _.union(scopes_permissions[scope], permissions);
     } else {
-      permissions[scope].splice(index, 1);
+      scopes_permissions[scope] = _.difference(scopes_permissions[scope], permissions);
+      if (_.isEmpty(scopes_permissions[scope])) {
+        _.unset(scopes_permissions, scope);
+      }
+    }
+    this.roleForm.get('scopes_permissions').setValue(scopes_permissions);
+  }
+
+  onClickHeaderCheckbox(property: 'scope' | 'read' | 'create' | 'update' | 'delete', event: Event) {
+    // Use a copy of the form field data to do not trigger the redrawing of the
+    // data table with every change.
+    const scopes_permissions = _.cloneDeep(this.roleForm.getValue('scopes_permissions'));
+    let permissions = [property];
+    if ('scope' === property) {
+      permissions = ['read', 'create', 'update', 'delete'];
     }
+    _.each(permissions, (permission) => {
+      _.each(this.scopes, (scope) => {
+        if (event.target['checked']) {
+          scopes_permissions[scope] = _.union(scopes_permissions[scope], [permission]);
+        } else {
+          scopes_permissions[scope] = _.difference(scopes_permissions[scope], [permission]);
+          if (_.isEmpty(scopes_permissions[scope])) {
+            _.unset(scopes_permissions, scope);
+          }
+        }
+      });
+    });
+    this.roleForm.get('scopes_permissions').setValue(scopes_permissions);
   }
 
   getRequest(): RoleFormModel {