You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@archiva.apache.org by ma...@apache.org on 2020/12/22 23:07:11 UTC

[archiva] 01/04: Adding role information

This is an automated email from the ASF dual-hosted git repository.

martin_s pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/archiva.git

commit 130e4a35e44c99fe0c6f6da95d21b76bcf8db1fc
Author: Martin Stockhammer <ma...@apache.org>
AuthorDate: Fri Dec 18 21:29:24 2020 +0100

    Adding role information
---
 .../src/main/archiva-web/src/app/model/role.ts     |   6 +
 .../app/modules/security/role-routing.module.ts    |   1 +
 .../manage-roles-edit.component.html               | 144 ++++++++++++++++++++-
 .../manage-roles-edit.component.ts                 | 142 ++++++++++++++++++--
 .../manage-roles-list.component.html               |   3 +
 .../manage-roles-list.component.ts                 |  11 +-
 .../roles/manage-roles/manage-roles.component.ts   |   1 -
 .../manage-users-edit.component.html               |   2 +-
 .../src/app/modules/shared/edit-base.component.ts  | 133 +++++++++++++++++++
 .../src/app/modules/shared/shared.module.ts        |   4 +-
 .../app/modules/shared/sorted-table-component.ts   |  11 ++
 .../archiva-web/src/app/services/role.service.ts   |   9 +-
 .../src/main/archiva-web/src/assets/i18n/en.json   |  20 ++-
 13 files changed, 465 insertions(+), 22 deletions(-)

diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/role.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/role.ts
index 819d0f1..ac0b469 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/role.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/role.ts
@@ -16,6 +16,8 @@
  * under the License.
  */
 
+import {Permission} from "@app/model/permission";
+
 export class Role {
     id: string
     name: string
@@ -29,7 +31,11 @@ export class Role {
     model_id:string
     resource:string
 
+    child_role_ids: Array<string>
+    parent_role_ids: Array<string>
     children: Array<Role>
+    parents: Array<Role>
+    permissions: Array<Permission>
 
     // Web Internal attributes
     enabled: boolean = true
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/role-routing.module.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/role-routing.module.ts
index 6452e65..1be49d7 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/role-routing.module.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/role-routing.module.ts
@@ -35,6 +35,7 @@ const routes: Routes = [
         children: [
             {path: 'list', component: ManageRolesListComponent},
             {path: 'edit/:roleid', component: ManageRolesEditComponent},
+            {path: 'edit', component: ManageRolesEditComponent},
             {path: '', redirectTo: 'list', pathMatch: 'full'}
         ]
     }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.html
index 292d9d5..d92d21c 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.html
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.html
@@ -16,4 +16,146 @@
   ~ under the License.
   -->
 
-<p>manage-roles-edit works!</p>
+<form class="mt-3 mb-3" [formGroup]="userForm" (ngSubmit)="onSubmit()">
+    <div class="form-group row col-md-8">
+        <div class="col-md-1" *ngIf="editRole && !editRole.permanent">{{'form.edit' |translate}}&nbsp;<span
+                class="fas fa-edit"></span></div>
+        <div class="col-md-6" *ngIf="editRole && !editRole.permanent">
+            <input class="form-check-input" type="checkbox" [value]="editMode" [checked]="editMode"
+                   (change)="editMode=!editMode"
+            >
+        </div>
+    </div>
+    <div class="form-group row col-md-8">
+        <label class="col-md-2 col-form-label" for="id">{{'roles.attributes.id' |translate}}</label>
+        <div class="col-md-6">
+            <input type="text" formControlName="id" id="id"
+                   [ngClass]="valid('id')"
+                   [attr.readonly]="true">
+        </div>
+    </div>
+    <div class="form-group row col-md-8">
+        <label class="col-md-2 col-form-label" for="id">{{'roles.attributes.name' |translate}}</label>
+        <div class="col-md-6">
+            <input type="text" formControlName="name" id="name"
+                   [ngClass]="valid('name')"
+                   [attr.readonly]="editMode?null:'true'">
+        </div>
+    </div>
+    <div class="form-group row col-md-8">
+        <label class="col-md-2 col-form-label" for="id">{{'roles.attributes.description' |translate}}</label>
+        <div class="col-md-6">
+            <input type="text" formControlName="description" id="description"
+                   [ngClass]="valid('description')"
+                   [attr.readonly]="editMode?null:'true'">
+        </div>
+    </div>
+    <div class="form-group row col-md-8">
+        <label class="col-md-2 col-form-label" for="id">{{'roles.attributes.resource' |translate}}</label>
+        <div class="col-md-6">
+            <input type="text" formControlName="resource" id="resource"
+                   [ngClass]="valid('resource')"
+                   [attr.readonly]="true">
+        </div>
+    </div>
+    <div class="form-group row col-md-8">
+        <div class="col-md-2"></div>
+        <div class="col-md-6">
+            <div class="form-check">
+                <input class="form-check-input" type="checkbox" formControlName="assignable"
+                       id="assignable" [attr.disabled]="true">
+                <label class="form-check-label " for="assignable">
+                    {{'roles.attributes.assignable'|translate}}
+                </label>
+            </div>
+            <div class="form-check">
+                <input class="form-check-input" type="checkbox" formControlName="template_instance"
+                       id="template_instance" [attr.disabled]="true">
+                <label class="form-check-label " for="template_instance">
+                    {{'roles.attributes.template_instance'|translate}}
+                </label>
+            </div>
+        </div>
+    </div>
+</form>
+<hr/>
+<ngb-accordion activeIds="parents,children,permissions">
+    <ngb-panel id="parents" >
+        <ng-template ngbPanelHeader let-opened="opened">
+            <div class="d-flex align-items-center justify-content-between">
+                <button ngbPanelToggle class="btn btn-link text-left shadow-none"><h3>{{'roles.edit.parents'|translate}}</h3></button>
+                <ng-container *ngIf="!opened"><i class="fa fa-eye-slash"></i></ng-container>
+                <ng-container *ngIf="opened"><i class="fa fa-eye"></i></ng-container>
+            </div>
+        </ng-template>
+        <ng-template ngbPanelContent>
+                <ng-container *ngIf="editRole?.parents.length>0">
+                    <ul class="list-group" *ngFor="let parentRole of editRole?.parents">
+                        <li class="list-group-item"><a routerLink="../{{parentRole?.id}}">{{parentRole?.name}}</a></li>
+                    </ul>
+                </ng-container>
+        </ng-template>
+    </ngb-panel>
+    <ngb-panel id="children">
+        <ng-template ngbPanelHeader let-opened="opened">
+            <div class="d-flex align-items-center justify-content-between">
+            <button ngbPanelToggle class="btn btn-link text-left shadow-none"><h3>{{'roles.edit.children'|translate}}</h3></button>
+            <ng-container *ngIf="!opened"><i class="fa fa-eye-slash"></i></ng-container>
+            <ng-container *ngIf="opened"><i class="fa fa-eye"></i></ng-container>
+            </div>
+        </ng-template>
+        <ng-template ngbPanelContent>
+                <ng-container *ngIf="editRole?.children.length>0">
+                    <ul class="list-group" *ngFor="let childRole of editRole?.children">
+                        <li class="list-group-item"><a routerLink="../{{childRole?.id}}">{{childRole?.name}}</a></li>
+                    </ul>
+                </ng-container>
+        </ng-template>
+    </ngb-panel>
+
+    <ngb-panel id="permissions">
+        <ng-template ngbPanelHeader let-opened="opened">
+            <div class="d-flex align-items-center justify-content-between">
+
+            <button ngbPanelToggle class="btn btn-link text-left shadow-none"><h3>{{'roles.edit.permissions'|translate}}</h3></button>
+            <ng-container *ngIf="!opened"><i class="fa fa-eye-slash"></i></ng-container>
+            <ng-container *ngIf="opened"><i class="fa fa-eye"></i></ng-container>
+            </div>
+        </ng-template>
+        <ng-template ngbPanelContent>
+                <ng-container *ngIf="editRole">
+                    <table class="table">
+                        <thead>
+                        <tr>
+                            <th>{{'permissions.attributes.permission'|translate}}</th>
+                            <th>{{'permissions.attributes.operation'|translate}}</th>
+                            <th>{{'permissions.attributes.resource'|translate}}</th>
+                        </tr>
+                        </thead>
+                        <tbody *ngFor="let perm of editRole.permissions">
+                        <tr>
+                            <td>{{perm.name}}</td>
+                            <td>{{perm.operation.name}}</td>
+                            <td>{{perm.resource.identifier}}</td>
+                        </tr>
+                        </tbody>
+
+                    </table>
+                </ng-container>
+        </ng-template>
+    </ngb-panel>
+
+    <ngb-panel>
+        <ng-template ngbPanelHeader let-opened="opened">
+            <div class="d-flex align-items-center justify-content-between">
+                <button ngbPanelToggle class="btn btn-link text-left shadow-none"><h3>{{'roles.edit.users'|translate}}</h3></button>
+                <ng-container *ngIf="!opened"><i class="fa fa-eye-slash"></i></ng-container>
+                <ng-container *ngIf="opened"><i class="fa fa-eye"></i></ng-container>
+            </div>
+        </ng-template>
+        <ng-template ngbPanelContent >
+             <h3>There are the users</h3>
+        </ng-template>
+    </ngb-panel>
+
+</ngb-accordion>
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.ts
index 4a642cb..cbbd5d5 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-edit/manage-roles-edit.component.ts
@@ -16,18 +16,144 @@
  * under the License.
  */
 
-import { Component, OnInit } from '@angular/core';
+import {Component, EventEmitter, OnInit, Output} from '@angular/core';
+import {ActivatedRoute} from "@angular/router";
+import {FormBuilder, Validators} from "@angular/forms";
+import {RoleService} from "@app/services/role.service";
+import {catchError, filter, map, switchMap, tap} from "rxjs/operators";
+import {Role} from '@app/model/role';
+import {ErrorResult} from "@app/model/error-result";
+import {EditBaseComponent} from "@app/modules/shared/edit-base.component";
+import {forkJoin, iif, Observable, of, pipe, zip} from 'rxjs';
 
 @Component({
-  selector: 'app-manage-roles-edit',
-  templateUrl: './manage-roles-edit.component.html',
-  styleUrls: ['./manage-roles-edit.component.scss']
+    selector: 'app-manage-roles-edit',
+    templateUrl: './manage-roles-edit.component.html',
+    styleUrls: ['./manage-roles-edit.component.scss']
 })
-export class ManageRolesEditComponent implements OnInit {
+export class ManageRolesEditComponent extends EditBaseComponent<Role> implements OnInit {
 
-  constructor() { }
+    parentsOpened: boolean
 
-  ngOnInit(): void {
-  }
+    editRole: Role;
+    editProperties = ['id', 'name', 'description', 'template_instance', 'resource', 'assignable'];
+    originRole;
+    roleCache: Map<string, Role> = new Map<string, Role>();
+
+
+    @Output()
+    roleIdEvent: EventEmitter<string> = new EventEmitter<string>(true);
+
+    constructor(private route: ActivatedRoute, private roleService: RoleService, public fb: FormBuilder) {
+        super(fb);
+        super.init(fb.group({
+            id: ['', [Validators.required]],
+            name: ['', Validators.required],
+            description: [''],
+            resource: [''],
+            template_instance: [''],
+            assignable: ['']
+        }, {}));
+    }
+
+    createEntity(): Role {
+        return new Role();
+    }
+
+    ngOnInit(): void {
+        this.route.params.pipe(
+            map(params => params.roleid),
+            filter(roleid => roleid != null),
+            tap(roleid => {
+                this.roleIdEvent.emit(roleid)
+            }),
+            switchMap((roleid: string) => this.roleService.getRole(roleid)),
+            switchMap((role: Role) => zip(of(role),
+                this.retrieveChildren(role),
+                this.retrieveParents(role))),
+            map((ra: [Role, Role[], Role[]]) => this.combine(ra))
+        ).subscribe(role => {
+            this.editRole = role;
+            this.originRole = role;
+            this.copyToForm(this.editProperties, this.editRole);
+        }, error => {
+            this.editRole = new Role();
+        });
+    }
+
+    /**
+     * Array of [role, children[], parents[]]
+     */
+    combine(roleArray: [Role, Role[], Role[]]): Role {
+        roleArray[0].children = roleArray[1];
+        roleArray[0].parents = roleArray[2];
+        return roleArray[0];
+    }
+
+    private createRole(id: string): Role {
+        let role = new Role();
+        role.id = id;
+        role.name=''
+        return role;
+    }
+
+    getCachedRole(id : string) : Observable<Role> {
+        return of(id).pipe(
+            switchMap(( myId : string )  => {
+                if (this.roleCache.has(myId)) {
+                    return of(this.roleCache.get(myId));
+                } else {
+                    return this.roleService.getRole(myId).pipe(tap(role => {
+                        this.roleCache.set(role.id, role);
+                    }),catchError(() => of(this.createRole(id))));
+                }
+            }));
+    }
+
+    retrieveChildren(role: Role): Observable<Role[]> {
+        // ForkJoin does not emit, if one of the observables is failing to emit a object
+        // -> we use catchError()
+        let children: Array<Observable<Role>> = []
+        for (let child_id of role.child_role_ids) {
+            children.push(this.getCachedRole(child_id));
+        }
+        if (children.length>0) {
+            return forkJoin(children);
+        } else {
+            return of([]);
+        }
+    }
+
+    retrieveParents(role: Role): Observable<Role[]> {
+        let parents: Array<Observable<Role>> = []
+        for (let parent_id of role.parent_role_ids) {
+            parents.push(this.getCachedRole(parent_id));
+        }
+        if (parents.length>0) {
+            return forkJoin(parents);
+        } else {
+            return of([]);
+        }
+    }
+
+    onSubmit() {
+        let role = this.copyFromForm(this.editProperties);
+        this.roleService.updateRole(role).pipe(
+            catchError((err: ErrorResult) => {
+                this.error = true;
+                this.success = false;
+                this.errorResult = err;
+                return [];
+            })
+        ).subscribe(roleInfo => {
+            this.error = false;
+            this.success = true;
+            this.errorResult = null;
+            this.result = roleInfo;
+            this.editMode = false;
+        });
+
+    }
 
 }
+
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.html
index 8b21e6c..7d904e7 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.html
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.html
@@ -26,6 +26,7 @@
             <app-th-sorted [fieldArray]="['id']" contentText="roles.attributes.id"></app-th-sorted>
             <app-th-sorted [fieldArray]="['name']" contentText="roles.attributes.name" ></app-th-sorted>
             <app-th-sorted [fieldArray]="['description']" contentText="roles.attributes.description" ></app-th-sorted>
+            <app-th-sorted [fieldArray]="['assignable']" contentText="roles.attributes.assignable"></app-th-sorted>
             <app-th-sorted [fieldArray]="['template_instance']" contentText="roles.attributes.template_instance" ></app-th-sorted>
             <app-th-sorted [fieldArray]="['resource']" contentText="roles.attributes.resource" ></app-th-sorted>
             <th>Action</th>
@@ -36,6 +37,8 @@
             <td><span data-toggle="tooltip" placement="left" ngbTooltip="{{role.id}}">{{role.id}}</span></td>
             <td>{{role.name}}</td>
             <td>{{role.description}}</td>
+            <td><span class="far" [attr.aria-valuetext]="role.assignable"
+                      [ngClass]="role.assignable?'fa-check-circle':'fa-circle'"></span></td>
             <td><span class="far" [attr.aria-valuetext]="role.template_instance"
                       [ngClass]="role.template_instance?'fa-check-circle':'fa-circle'"></span></td>
             <td>{{role.resource}}</td>
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.ts
index e3998d7..b49f64d 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles-list/manage-roles-list.component.ts
@@ -25,21 +25,20 @@ import {Observable} from "rxjs";
 import {PagedResult} from "@app/model/paged-result";
 import {UserInfo} from "@app/model/user-info";
 import {RoleService} from "@app/services/role.service";
+import {SortedTableComponent} from "@app/modules/shared/sorted-table-component";
 
 @Component({
   selector: 'app-manage-roles-list',
   templateUrl: './manage-roles-list.component.html',
   styleUrls: ['./manage-roles-list.component.scss']
 })
-export class ManageRolesListComponent implements OnInit {
+export class ManageRolesListComponent extends SortedTableComponent<Role> implements OnInit {
 
-  service: EntityService<Role>
-
-  constructor(private translator: TranslateService, private roleService : RoleService) {
-    this.service = function (searchTerm: string, offset: number, limit: number, orderBy: string[], order: string) : Observable<PagedResult<Role>> {
+  constructor(translator: TranslateService, roleService : RoleService) {
+    super(translator, function (searchTerm: string, offset: number, limit: number, orderBy: string[], order: string): Observable<PagedResult<Role>> {
       console.log("Retrieving data " + searchTerm + "," + offset + "," + limit + "," + orderBy + "," + order);
       return roleService.query(searchTerm, offset, limit, orderBy, order);
-    }
+    });
   }
 
   ngOnInit(): void {
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles/manage-roles.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles/manage-roles.component.ts
index f2a67dd..74af9c2 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles/manage-roles.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/roles/manage-roles/manage-roles.component.ts
@@ -39,7 +39,6 @@ export class ManageRolesComponent implements OnInit {
     // console.log("Activating "+componentReference+" - "+JSON.stringify(componentReference,getCircularReplacer()))
     if (componentReference.roleIdEvent!=null) {
       let componentEmit : Observable<string> = componentReference.roleIdEvent.pipe(
-          tap(userid=>console.log("Event "+componentReference.class+" "+userid)),
           map((userid: string) => this.getSubPath(userid)));
       if (this.roleId$!=null) {
         this.roleId$ = merge(this.roleId$, componentEmit)
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-edit/manage-users-edit.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-edit/manage-users-edit.component.html
index ba8becf..8f12e3b 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-edit/manage-users-edit.component.html
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/users/manage-users-edit/manage-users-edit.component.html
@@ -18,7 +18,7 @@
 
 <form class="mt-3 mb-3" [formGroup]="userForm" (ngSubmit)="onSubmit()">
     <div class="form-group row col-md-8" *ngIf="!editUser.permanent">
-        <div class="col-md-1">Edit <span class="fas fa-edit"></span></div>
+        <div class="col-md-1">{{'form.edit' |translate}} <span class="fas fa-edit"></span></div>
         <div class="col-md-6">
             <input class="form-check-input" type="checkbox" [value]="editMode" [checked]="editMode"
                    (change)="editMode=!editMode"
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/edit-base.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/edit-base.component.ts
new file mode 100644
index 0000000..29a1d0f
--- /dev/null
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/edit-base.component.ts
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {AbstractControl, FormBuilder, FormControl, FormGroup, ValidationErrors, ValidatorFn} from "@angular/forms";
+import {ErrorResult} from '@app/model/error-result';
+
+export abstract class EditBaseComponent<T> {
+
+    editProperties = ['id'];
+    success: boolean = false;
+    error: boolean = false;
+    errorResult: ErrorResult;
+    result: T;
+    formInitialValues;
+    public userForm : FormGroup;
+    public editMode: boolean;
+
+    constructor(public fb: FormBuilder) {
+
+    }
+
+    init(userForm: FormGroup) : void {
+        this.userForm=userForm;
+        this.formInitialValues = userForm.value;
+    }
+
+    abstract createEntity() : T;
+    abstract onSubmit();
+
+    public copyFromForm(properties: string[]): T {
+        let entity: any = this.createEntity();
+        for (let prop of properties) {
+            entity[prop] = this.userForm.get(prop).value;
+        }
+        return entity;
+    }
+
+    public copyToForm(properties: string[], user: T): void {
+        let propMap = {};
+        for (let prop of properties) {
+            let propValue = user[prop] == null ? '' : user[prop];
+            propMap[prop] = propValue;
+        }
+        this.userForm.patchValue(propMap);
+    }
+
+
+    valid(field: string): string[] {
+        if (this.editMode) {
+            let classArr = this.isValid(field);
+            return classArr.concat('form-control')
+        } else {
+            return ['form-control-plaintext'];
+        }
+    }
+
+    isValid(field: string): string[] {
+        let formField = this.userForm.get(field);
+        if (formField.dirty || formField.touched) {
+            if (formField.valid) {
+                return ['is-valid']
+            } else {
+                return ['is-invalid']
+            }
+        } else {
+            return ['']
+        }
+    }
+
+    forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
+        return (control: AbstractControl): { [key: string]: any } | null => {
+            const forbidden = nameRe.test(control.value);
+            return forbidden ? {forbiddenName: {value: control.value}} : null;
+        };
+    }
+
+    getAllErrors(formGroup: FormGroup, errors: string[] = []) : string[] {
+        Object.keys(formGroup.controls).forEach(field => {
+            const control = formGroup.get(field);
+            if (control instanceof FormControl && control.errors != null) {
+                let keys = Object.keys(control.errors).map(errorKey=>field+'.'+errorKey);
+                errors = errors.concat(keys);
+            } else if (control instanceof FormGroup) {
+                errors = errors.concat(this.getAllErrors(control));
+            }
+        });
+        return errors;
+    }
+
+    getAttributeErrors(control:string):string[] {
+        return Object.keys(this.userForm.get(control).errors);
+    }
+}
+
+export function whitespaceValidator(): ValidatorFn {
+    return (control: AbstractControl): ValidationErrors | null => {
+        const hasWhitespace =  /\s/g.test(control.value);
+        return hasWhitespace ? {containsWhitespace: {value: control.value}} : null;
+    };
+}
+export function MustMatch(controlName: string, matchingControlName: string) : ValidatorFn  {
+    return (formGroup: FormGroup): ValidationErrors | null => {
+        const control = formGroup.controls[controlName];
+        const matchingControl = formGroup.controls[matchingControlName];
+
+        if (matchingControl.errors && !matchingControl.errors.mustMatch) {
+            // return if another validator has already found an error on the matchingControl
+            return;
+        }
+
+        // set error on matchingControl if validation fails
+        if (control.value !== matchingControl.value) {
+            matchingControl.setErrors({mustMatch: true});
+        } else {
+            matchingControl.setErrors(null);
+        }
+    }
+}
\ No newline at end of file
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/shared.module.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/shared.module.ts
index 1030390..27978a3 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/shared.module.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/shared.module.ts
@@ -21,12 +21,13 @@ import {CommonModule} from '@angular/common';
 import {PaginatedEntitiesComponent} from "./paginated-entities/paginated-entities.component";
 import {SortedTableHeaderComponent} from "./sorted-table-header/sorted-table-header.component";
 import {SortedTableHeaderRowComponent} from "./sorted-table-header-row/sorted-table-header-row.component";
-import {NgbPaginationModule, NgbTooltipModule} from "@ng-bootstrap/ng-bootstrap";
+import {NgbAccordionModule, NgbPaginationModule, NgbTooltipModule} from "@ng-bootstrap/ng-bootstrap";
 import {TranslateCompiler, TranslateLoader, TranslateModule} from "@ngx-translate/core";
 import {TranslateMessageFormatCompiler} from "ngx-translate-messageformat-compiler";
 import {HttpClient} from "@angular/common/http";
 import {TranslateHttpLoader} from "@ngx-translate/http-loader";
 import {RouterModule} from "@angular/router";
+import {SortedTableComponent} from "@app/modules/shared/sorted-table-component";
 
 
 @NgModule({
@@ -41,6 +42,7 @@ import {RouterModule} from "@angular/router";
         TranslateModule,
         NgbPaginationModule,
         NgbTooltipModule,
+        NgbAccordionModule,
         PaginatedEntitiesComponent,
         SortedTableHeaderComponent,
         SortedTableHeaderRowComponent
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sorted-table-component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sorted-table-component.ts
new file mode 100644
index 0000000..27423fa
--- /dev/null
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/sorted-table-component.ts
@@ -0,0 +1,11 @@
+import {EntityService} from "@app/model/entity-service";
+import {TranslateService} from "@ngx-translate/core";
+
+export class SortedTableComponent<T> {
+
+    sortField = ["id"];
+    sortOrder = "asc";
+
+    constructor(public translator : TranslateService, public service: EntityService<T>) {
+    }
+}
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/role.service.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/role.service.ts
index 79bee94..3de9919 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/role.service.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/role.service.ts
@@ -45,7 +45,6 @@ export class RoleService {
   }
 
   public query(searchTerm: string, offset: number = 0, limit: number = 10, orderBy: string[] = ['id'], order: string = 'asc'): Observable<PagedResult<Role>> {
-    console.log("getRoleList " + searchTerm + "," + offset + "," + limit + "," + orderBy + "," + order);
     if (searchTerm == null) {
       searchTerm = ""
     }
@@ -61,4 +60,12 @@ export class RoleService {
     });
   }
 
+  public getRole(roleId:string) : Observable<Role> {
+    return this.rest.executeRestCall("get", "redback", "roles/" + roleId, null);
+  }
+
+  public updateRole(role:Role) : Observable<Role> {
+    return this.rest.executeRestCall("put", "redback", "roles/" + role.id, role);
+  }
+
 }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json
index 6c39c16..7bdf9b8 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json
@@ -132,16 +132,29 @@
       "head": "Roles List"
     },
     "edit": {
-      "head": "Edit/View Role"
+      "head": "Edit/View Role",
+      "parents": "Parents",
+      "children": "Children",
+      "permissions": "Permissions",
+      "users": "Users"
     },
     "attributes": {
       "id": "Identifier",
       "name": "Name",
       "description": "Description",
-      "template_instance": "Template Instance"
+      "template_instance": "Template Instance",
+      "resource": "Repository",
+      "assignable": "Assignable"
     }
   },
 
+  "permissions": {
+    "attributes": {
+      "permission": "Permission",
+      "operation": "Operation",
+      "resource": "Resource"
+    }
+  },
   "search": {
     "button": "Search",
     "label": "Enter your search term",
@@ -158,7 +171,8 @@
       "yes": "Yes",
       "no": "No",
       "save": "Save Changes"
-    }
+    },
+    "edit": "Edit"
   },
   "password": {
     "violations" : {