feat(console): authorizations from user context, add grants to auth user view, fix app saving (#895)

* user grant on project

* grant in auth user, enable creation, fix inv regex

* use autocomplete solutions, section for usre ctx

* user grant create for user context

* fix edit from table

* fix create context

* fix authorization to write

* grant overview component

* fix user grants without context

* lint

* turn table highlighting off, rm logs

* fix app name saving

* fix table refresh for project grants

* translate toast

* i18n

* lint

* Update console/src/assets/i18n/de.json

Co-authored-by: Florian Forster <florian@caos.ch>

Co-authored-by: Florian Forster <florian@caos.ch>
This commit is contained in:
Max Peintner 2020-10-27 10:29:14 +01:00 committed by GitHub
parent afa38aa2c2
commit 78e5c26015
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 483 additions and 178 deletions

View File

@ -65,6 +65,14 @@ const routes: Routes = [
roles: ['org.read'], roles: ['org.read'],
}, },
}, },
{
path: 'grants',
loadChildren: () => import('./pages/grants/grants.module').then(m => m.GrantsModule),
canActivate: [AuthGuard, RoleGuard],
data: {
roles: ['user.grant.read'],
},
},
{ {
path: 'grant-create', path: 'grant-create',
canActivate: [AuthGuard], canActivate: [AuthGuard],
@ -87,6 +95,24 @@ const routes: Routes = [
roles: ['user.grant.write'], roles: ['user.grant.write'],
}, },
}, },
{
path: 'user/:userid',
loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module')
.then(m => m.UserGrantCreateModule),
canActivate: [RoleGuard],
data: {
roles: ['user.grant.write'],
},
},
{
path: '',
loadChildren: () => import('src/app/pages/user-grant-create/user-grant-create.module')
.then(m => m.UserGrantCreateModule),
canActivate: [RoleGuard],
data: {
roles: ['user.grant.write'],
},
},
], ],
}, },
{ {

View File

@ -138,6 +138,21 @@
<span class="label">{{ 'MENU.MACHINEUSERS' | translate }}</span> <span class="label">{{ 'MENU.MACHINEUSERS' | translate }}</span>
</a> </a>
</ng-template> </ng-template>
<ng-template appHasRole [appHasRole]="['user.grant.read(:[0-9]*)?']">
<div @navitem class="divider">
<div class="line"></div>
<span class="label">
{{ 'MENU.GRANTSECTION' | translate }}</span>
<div class="line"></div>
</div>
<a @navitem class="nav-item" [routerLinkActive]="['active']" [routerLink]="[ '/grants']"
[routerLinkActiveOptions]="{ exact: true }">
<i class="icon las la-shield-alt"></i>
<span class="label">{{ 'MENU.GRANTS' | translate }}</span>
</a>
</ng-template>
</div> </div>
<span class="fill-space"></span> <span class="fill-space"></span>

View File

@ -212,7 +212,7 @@
padding: 2px 1rem; padding: 2px 1rem;
border-radius: 50vw; border-radius: 50vw;
color: var(--grey); color: var(--grey);
font-size: 12px; font-size: 11px;
} }
.line { .line {

View File

@ -1,5 +1,5 @@
<app-refresh-table [loading]="loading$ | async" (refreshed)="refreshPage()" [dataSize]="dataSource.data.length" <app-refresh-table [loading]="loading$ | async" (refreshed)="refreshPage()" [dataSize]="dataSource.data.length"
emitRefreshOnPreviousRoute="/iam/idp/create" [timestamp]="idpResult?.viewTimestamp" [selection]="selection"> [emitRefreshOnPreviousRoutes]="['/iam/idp/create']" [timestamp]="idpResult?.viewTimestamp" [selection]="selection">
<ng-template appHasRole [appHasRole]="['iam.write']" actions> <ng-template appHasRole [appHasRole]="['iam.write']" actions>
<button (click)="deactivateSelectedIdps()" matTooltip="{{'IDP.DEACTIVATE' | translate}}" class="icon-button" <button (click)="deactivateSelectedIdps()" matTooltip="{{'IDP.DEACTIVATE' | translate}}" class="icon-button"
mat-icon-button *ngIf="selection.hasValue()" [disabled]="disabled"> mat-icon-button *ngIf="selection.hasValue()" [disabled]="disabled">
@ -101,4 +101,4 @@
</div> </div>
<mat-paginator #paginator class="paginator" [length]="idpResult?.totalResult || 0" [pageSize]="10" <mat-paginator #paginator class="paginator" [length]="idpResult?.totalResult || 0" [pageSize]="10"
[pageSizeOptions]="[5, 10, 20]" (page)="changePage($event)"></mat-paginator> [pageSizeOptions]="[5, 10, 20]" (page)="changePage($event)"></mat-paginator>
</app-refresh-table> </app-refresh-table>

View File

@ -1,5 +1,5 @@
<app-refresh-table *ngIf="projectId" (refreshed)="refreshPage()" [dataSize]="dataSource?.totalResult" <app-refresh-table *ngIf="projectId" (refreshed)="refreshPage()" [dataSize]="dataSource?.totalResult"
emitRefreshOnPreviousRoute="/projects/{{projectId}}/roles/create" [selection]="selection" [emitRefreshOnPreviousRoutes]="['/projects/'+projectId+'/roles/create']" [selection]="selection"
[loading]="dataSource?.loading$ | async" [timestamp]="dataSource?.viewTimestamp"> [loading]="dataSource?.loading$ | async" [timestamp]="dataSource?.viewTimestamp">
<ng-template appHasRole [appHasRole]="['project.role.delete', 'project.role.delete:' + projectId]" actions> <ng-template appHasRole [appHasRole]="['project.role.delete', 'project.role.delete:' + projectId]" actions>
<button color="warn" class="icon-button" [disabled]="disabled" <button color="warn" class="icon-button" [disabled]="disabled"

View File

@ -33,7 +33,7 @@ export class RefreshTableComponent implements OnInit {
@Input() public dataSize: number = 0; @Input() public dataSize: number = 0;
@Input() public emitRefreshAfterTimeoutInMs: number = 0; @Input() public emitRefreshAfterTimeoutInMs: number = 0;
@Input() public loading: boolean = false; @Input() public loading: boolean = false;
@Input() public emitRefreshOnPreviousRoute: string = ''; @Input() public emitRefreshOnPreviousRoutes: string[] = [];
@Output() public refreshed: EventEmitter<void> = new EventEmitter(); @Output() public refreshed: EventEmitter<void> = new EventEmitter();
constructor(private refreshService: RefreshService) { } constructor(private refreshService: RefreshService) { }
@ -45,7 +45,8 @@ export class RefreshTableComponent implements OnInit {
}, this.emitRefreshAfterTimeoutInMs); }, this.emitRefreshAfterTimeoutInMs);
} }
if (this.emitRefreshOnPreviousRoute && this.refreshService.previousUrls.includes(this.emitRefreshOnPreviousRoute)) { if (this.emitRefreshOnPreviousRoutes.length && this.refreshService.previousUrls
.some(url => this.emitRefreshOnPreviousRoutes.includes(url))) {
setTimeout(() => { setTimeout(() => {
console.log('refresh now'); console.log('refresh now');
this.emitRefresh(); this.emitRefresh();

View File

@ -1,5 +1,5 @@
<form> <form>
<mat-form-field *ngIf="target == UserTarget.SELF" appearance="outline" class="full-width"> <mat-form-field *ngIf="target && target == UserTarget.SELF" appearance="outline" class="full-width">
<mat-label>Organizations User Loginname</mat-label> <mat-label>Organizations User Loginname</mat-label>
<input matInput *ngIf="singleOutput" type="text" placeholder="Search for the user loginname" #usernameInput <input matInput *ngIf="singleOutput" type="text" placeholder="Search for the user loginname" #usernameInput
@ -28,12 +28,21 @@
<small>{{user.preferredLoginName}}</small> <small>{{user.preferredLoginName}}</small>
</mat-option> </mat-option>
</mat-autocomplete> </mat-autocomplete>
<mat-hint class="target-desc">
{{'USER.TARGET.SELF'| translate}}
<a (click)="changeTarget()">{{'USER.TARGET.CLICKHERE' | translate}}</a>
</mat-hint>
</mat-form-field> </mat-form-field>
<div *ngIf="target == UserTarget.EXTERNAL" class="line"> <div *ngIf="target && target == UserTarget.EXTERNAL" class="line">
<mat-form-field class="form-field" appearance="outline"> <mat-form-field class="form-field" appearance="outline">
<mat-label>Global User Loginname</mat-label> <mat-label>Global User Loginname</mat-label>
<input matInput type="text" [formControl]="globalLoginNameControl" /> <input matInput type="text" [formControl]="globalLoginNameControl" />
<mat-hint class="target-desc">
{{(target == UserTarget.SELF ? 'USER.TARGET.SELF' : 'USER.TARGET.EXTERNAL') | translate}}
<a (click)="changeTarget()">{{'USER.TARGET.CLICKHERE' | translate}}</a>
</mat-hint>
</mat-form-field> </mat-form-field>
<button color="primary" mat-icon-button (click)="getGlobalUser()"> <button color="primary" mat-icon-button (click)="getGlobalUser()">
@ -41,17 +50,25 @@
</button> </button>
</div> </div>
<div *ngIf="target == UserTarget.EXTERNAL && users.length > 0"> <div class="found" *ngIf="target == UserTarget.EXTERNAL && users.length > 0">
<span class="found-label">{{'USER.SEARCH.FOUND' | translate}}:</span> <span class="found-label">{{'USER.SEARCH.FOUND' | translate}}:</span>
<div class="found-user-row" *ngFor="let user of users; index as i"> <div class="found-user-row" *ngFor="let user of users; index as i">
<div class="circle">
<app-avatar
*ngIf="user.human && user.human.displayName && user.human?.firstName && user.human?.lastName; else cog"
class="avatar" [name]="user.human.displayName" [size]="32">
</app-avatar>
<ng-template #cog>
<div class="sa-icon">
<i class="las la-user-cog"></i>
</div>
</ng-template>
</div>
<span>{{user.preferredLoginName}}</span> <span>{{user.preferredLoginName}}</span>
<button mat-icon-button> <button mat-icon-button color="warn">
<i class="las la-minus-circle" (click)="users.splice(i, 1)"></i> <i class="las la-minus-circle" (click)="users.splice(i, 1)"></i>
</button> </button>
</div> </div>
</div> </div>
<p class="target-desc">{{(target == UserTarget.SELF ? 'USER.TARGET.SELF' : 'USER.TARGET.EXTERNAL') | translate}}
<a (click)="changeTarget()">{{'USER.TARGET.CLICKHERE' | translate}}</a>
</p>
</form> </form>

View File

@ -3,11 +3,6 @@
} }
.target-desc { .target-desc {
color: var(--grey);
font-size: .8rem;
margin: 0;
margin-bottom: 1rem;
a { a {
color: #4072b4; color: #4072b4;
@ -37,12 +32,35 @@
font-size: .8rem; font-size: .8rem;
} }
.found-user-row { .found {
display: flex; margin: .5rem 0;
align-items: center; background: #4072b410;
border-radius: .5rem;
padding: .5rem;
.found-user-row {
padding: .5rem 0;
display: flex;
align-items: center;
min-height: 56px;
button {
visibility: hidden;
}
&:hover {
button {
visibility: visible;
}
}
}
.found-label {
font-size: .9rem;
color: var(--grey);
}
} }
.found-label { .circle {
font-size: .9rem; margin-right: .5rem;
color: var(--grey);
} }

View File

@ -1,5 +1,15 @@
import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; import {
AfterContentChecked,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
Input,
OnInit,
Output,
ViewChild,
} from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatChipInputEvent } from '@angular/material/chips'; import { MatChipInputEvent } from '@angular/material/chips';
@ -19,7 +29,7 @@ export enum UserTarget {
templateUrl: './search-user-autocomplete.component.html', templateUrl: './search-user-autocomplete.component.html',
styleUrls: ['./search-user-autocomplete.component.scss'], styleUrls: ['./search-user-autocomplete.component.scss'],
}) })
export class SearchUserAutocompleteComponent { export class SearchUserAutocompleteComponent implements OnInit, AfterContentChecked {
public selectable: boolean = true; public selectable: boolean = true;
public removable: boolean = true; public removable: boolean = true;
public addOnBlur: boolean = true; public addOnBlur: boolean = true;
@ -32,7 +42,7 @@ export class SearchUserAutocompleteComponent {
@Input() public users: Array<UserView.AsObject> = []; @Input() public users: Array<UserView.AsObject> = [];
public filteredUsers: Array<UserView.AsObject> = []; public filteredUsers: Array<UserView.AsObject> = [];
public isLoading: boolean = false; public isLoading: boolean = false;
public target: UserTarget = UserTarget.SELF; @Input() public target: UserTarget = UserTarget.SELF;
public hint: string = ''; public hint: string = '';
public UserTarget: any = UserTarget; public UserTarget: any = UserTarget;
@ViewChild('usernameInput') public usernameInput!: ElementRef<HTMLInputElement>; @ViewChild('usernameInput') public usernameInput!: ElementRef<HTMLInputElement>;
@ -41,8 +51,19 @@ export class SearchUserAutocompleteComponent {
@Input() public singleOutput: boolean = false; @Input() public singleOutput: boolean = false;
private unsubscribed$: Subject<void> = new Subject(); private unsubscribed$: Subject<void> = new Subject();
constructor(private userService: ManagementService, private toast: ToastService) { constructor(private userService: ManagementService, private toast: ToastService, private cdref: ChangeDetectorRef) { }
this.getFilteredResults();
public ngOnInit(): void {
if (this.target === UserTarget.EXTERNAL) {
this.filteredUsers = [];
this.unsubscribed$.next(); // clear old subscription
} else if (this.target === UserTarget.SELF) {
this.getFilteredResults(); // new subscription
}
}
public ngAfterContentChecked(): void {
this.cdref.detectChanges();
} }
private getFilteredResults(): void { private getFilteredResults(): void {
@ -143,10 +164,11 @@ export class SearchUserAutocompleteComponent {
public getGlobalUser(): void { public getGlobalUser(): void {
this.userService.GetUserByLoginNameGlobal(this.globalLoginNameControl.value).then(user => { this.userService.GetUserByLoginNameGlobal(this.globalLoginNameControl.value).then(user => {
this.users = [user.toObject()];
if (this.singleOutput) { if (this.singleOutput) {
this.users = [user.toObject()];
this.selectionChanged.emit(this.users[0]); this.selectionChanged.emit(this.users[0]);
} else { } else {
this.users.push(user.toObject());
this.selectionChanged.emit(this.users); this.selectionChanged.emit(this.users);
} }
}).catch(error => { }).catch(error => {

View File

@ -11,6 +11,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { AvatarModule } from '../avatar/avatar.module';
import { SearchUserAutocompleteComponent } from './search-user-autocomplete.component'; import { SearchUserAutocompleteComponent } from './search-user-autocomplete.component';
@ -29,6 +30,7 @@ import { SearchUserAutocompleteComponent } from './search-user-autocomplete.comp
FormsModule, FormsModule,
TranslateModule, TranslateModule,
MatSelectModule, MatSelectModule,
AvatarModule,
], ],
exports: [SearchUserAutocompleteComponent], exports: [SearchUserAutocompleteComponent],
}) })

View File

@ -13,7 +13,7 @@ import {
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
export enum UserGrantContext { export enum UserGrantContext {
// AUTHUSER = 'authuser', NONE = 'none',
USER = 'user', USER = 'user',
OWNED_PROJECT = 'owned', OWNED_PROJECT = 'owned',
GRANTED_PROJECT = 'granted', GRANTED_PROJECT = 'granted',
@ -42,14 +42,13 @@ export class UserGrantsDataSource extends DataSource<UserGrant.AsObject> {
}, },
queries?: UserGrantSearchQuery[], queries?: UserGrantSearchQuery[],
): void { ): void {
const offset = pageIndex * pageSize;
switch (context) { switch (context) {
case UserGrantContext.USER: case UserGrantContext.USER:
if (data && data.userId) { if (data && data.userId) {
this.loadingSubject.next(true); this.loadingSubject.next(true);
const userfilter = new UserGrantSearchQuery(); const userfilter = new UserGrantSearchQuery();
userfilter.setKey(UserGrantSearchKey.USERGRANTSEARCHKEY_USER_ID); userfilter.setKey(UserGrantSearchKey.USERGRANTSEARCHKEY_USER_ID);
userfilter.setMethod(SearchMethod.SEARCHMETHOD_EQUALS);
userfilter.setValue(data.userId); userfilter.setValue(data.userId);
if (queries) { if (queries) {
queries.push(userfilter); queries.push(userfilter);
@ -57,7 +56,7 @@ export class UserGrantsDataSource extends DataSource<UserGrant.AsObject> {
queries = [userfilter]; queries = [userfilter];
} }
const promise = this.userService.SearchUserGrants(10, 0, queries); const promise = this.userService.SearchUserGrants(pageSize, pageSize * pageIndex, queries);
this.loadResponse(promise); this.loadResponse(promise);
} }
break; break;
@ -66,6 +65,7 @@ export class UserGrantsDataSource extends DataSource<UserGrant.AsObject> {
this.loadingSubject.next(true); this.loadingSubject.next(true);
const projectfilter = new UserGrantSearchQuery(); const projectfilter = new UserGrantSearchQuery();
projectfilter.setKey(UserGrantSearchKey.USERGRANTSEARCHKEY_PROJECT_ID); projectfilter.setKey(UserGrantSearchKey.USERGRANTSEARCHKEY_PROJECT_ID);
projectfilter.setMethod(SearchMethod.SEARCHMETHOD_EQUALS);
projectfilter.setValue(data.projectId); projectfilter.setValue(data.projectId);
if (queries) { if (queries) {
queries.push(projectfilter); queries.push(projectfilter);
@ -73,7 +73,7 @@ export class UserGrantsDataSource extends DataSource<UserGrant.AsObject> {
queries = [projectfilter]; queries = [projectfilter];
} }
const promise1 = this.userService.SearchUserGrants(10, 0, queries); const promise1 = this.userService.SearchUserGrants(pageSize, pageSize * pageIndex, queries);
this.loadResponse(promise1); this.loadResponse(promise1);
} }
break; break;
@ -97,10 +97,15 @@ export class UserGrantsDataSource extends DataSource<UserGrant.AsObject> {
queries = [projectfilter, grantquery]; queries = [projectfilter, grantquery];
} }
const promise2 = this.userService.SearchUserGrants(10, 0, queries); const promise2 = this.userService.SearchUserGrants(pageSize, pageSize * pageIndex, queries);
this.loadResponse(promise2); this.loadResponse(promise2);
} }
break; break;
default:
this.loadingSubject.next(true);
const promise3 = this.userService.SearchUserGrants(pageSize, pageSize * pageIndex, []);
this.loadResponse(promise3);
break;
} }
} }

View File

@ -1,12 +1,12 @@
<app-refresh-table [loading]="dataSource?.loading$ | async" (refreshed)="changePage()" <app-refresh-table [loading]="dataSource?.loading$ | async" (refreshed)="changePage()"
[emitRefreshOnPreviousRoute]="refreshOnPreviousRoute" [timestamp]="dataSource?.viewTimestamp" [emitRefreshOnPreviousRoutes]="refreshOnPreviousRoutes" [timestamp]="dataSource?.viewTimestamp"
[dataSize]="dataSource?.totalResult" [selection]="selection"> [dataSize]="dataSource?.totalResult" [selection]="selection">
<button color="warn" matTooltip="{{'GRANTS.DELETE' | translate}}" class="icon-button" mat-icon-button actions <button color="warn" matTooltip="{{'GRANTS.DELETE' | translate}}" class="icon-button" mat-icon-button actions
(click)="deleteGrantSelection()" *ngIf="selection.hasValue() && allowDelete"> (click)="deleteGrantSelection()" *ngIf="selection.hasValue() && disableDelete == false">
<i class="las la-trash"></i> <i class="las la-trash"></i>
</button> </button>
<a *ngIf="allowWrite && context !== UserGrantContext.USER" matTooltip="{{'GRANTS.ADD' | translate}}" actions <a *ngIf="disableWrite == false" matTooltip="{{'GRANTS.ADD' | translate}}" actions color="primary" color="primary"
color="primary" color="primary" mat-raised-button [routerLink]="routerLink"> mat-raised-button [routerLink]="routerLink">
<mat-icon class="icon">add</mat-icon>{{ 'GRANTS.ADD_BTN' | translate }} <mat-icon class="icon">add</mat-icon>{{ 'GRANTS.ADD_BTN' | translate }}
</a> </a>
@ -14,75 +14,81 @@
<table mat-table multiTemplateDataRows class="table" aria-label="Elements" [dataSource]="dataSource"> <table mat-table multiTemplateDataRows class="table" aria-label="Elements" [dataSource]="dataSource">
<ng-container matColumnDef="select"> <ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef> <th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox [disabled]="allowWrite == false" color="primary" <mat-checkbox [disabled]="disableWrite" color="primary" (change)="$event ? masterToggle() : null"
(change)="$event ? masterToggle() : null" [checked]="selection.hasValue() && isAllSelected()" [checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()"> [indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox> </mat-checkbox>
</th> </th>
<td class="selection" mat-cell *matCellDef="let row"> <td class="selection" mat-cell *matCellDef="let row">
<mat-checkbox [disabled]="allowWrite == false" color="primary" (click)="$event.stopPropagation()" <mat-checkbox
[disabled]="disableWrite || !((['user.grant.write$'] | hasRole | async) || ((context === UserGrantContext.OWNED_PROJECT ? ['user.grant.write:' + row?.projectId] : context === UserGrantContext.GRANTED_PROJECT ? ['user.grant.write:' + row?.grantId] : []) | hasRole | async))"
color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)"> (change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
<app-avatar *ngIf="row && row?.displayName && row.firstName && row.lastName" class="avatar" <app-avatar
[name]="row.displayName" [size]="32"> *ngIf="context !== UserGrantContext.USER && row && row?.displayName && row.firstName && row.lastName"
class="avatar" [name]="row.displayName" [size]="32">
</app-avatar> </app-avatar>
</mat-checkbox> </mat-checkbox>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="user"> <ng-container matColumnDef="user">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.USER' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.USER' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let grant"> <td mat-cell *matCellDef="let grant">
{{grant?.displayName}}</td> {{grant?.displayName}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="org"> <ng-container matColumnDef="org">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.GRANTEDORGDOMAIN' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.GRANTEDORGDOMAIN' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let grant"> <td mat-cell *matCellDef="let grant">
{{grant.orgName}} </td> {{grant.orgName}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="projectId"> <ng-container matColumnDef="projectId">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.PROJECTNAME' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.PROJECTNAME' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let grant"> <td mat-cell *matCellDef="let grant">
{{grant.projectName}} </td> {{grant.projectName}} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="creationDate"> <ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.CREATIONDATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.CREATIONDATE' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let grant"> <td mat-cell *matCellDef="let grant">
{{grant.creationDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }} </td> {{grant.creationDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="changeDate"> <ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.CHANGEDATE' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.CHANGEDATE' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let grant"> <td mat-cell *matCellDef="let grant">
{{grant.changeDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }} </td> {{grant.changeDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="roleNamesList"> <ng-container matColumnDef="roleNamesList">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }} </th> <th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }} </th>
<td mat-cell *matCellDef="let grant; let i = index"> <td mat-cell *matCellDef="let grant; let i = index">
<ng-container *ngIf="context === UserGrantContext.USER"> <ng-container *ngIf="context === UserGrantContext.USER || context === UserGrantContext.NONE">
<span class="no-roles" <ng-container
*ngIf="grant.roleKeysList?.length === 0">{{'PROJECT.GRANT.NOROLES' | translate}}</span> *ngIf="(grant.grantId && loadedGrantId !== grant.grantId) || (loadedProjectId !== grant.projectId)">
<span <div class="flex-row">
*ngFor="let role of grant.roleKeysList">{{ (role.length>8)? (role | slice:0:8)+'..':(role) }}</span> <span class="role" *ngFor="let role of grant.roleKeysList">{{ role }}</span>
<button mat-stroked-button
*ngIf="grant.grantId ? loadedGrantId !== grant.grantId : loadedProjectId !== grant.projectId"
[disabled]="disableWrite || !((['user.grant.write$'] | hasRole | async) || ((context === UserGrantContext.OWNED_PROJECT ? ['user.grant.write:' + grant?.projectId] : context === UserGrantContext.GRANTED_PROJECT ? ['user.grant.write:' + grant?.grantId] : []) | hasRole | async))"
(click)="grant.grantId ? getGrantRoleOptions(grant.grantId, grant.projectId) : getProjectRoleOptions(grant.projectId)"
matTooltip="{{'ACTIONS.CHANGE' | translate}}">
<i class="las la-edit"></i>
{{'ACTIONS.EDIT' | translate}}
</button>
</div>
</ng-container>
</ng-container> </ng-container>
<ng-container *ngIf="context === UserGrantContext.OWNED_PROJECT"> <ng-container
<ng-container *ngIf="loadedProjectId !== grant.projectId"> *ngIf="context === UserGrantContext.OWNED_PROJECT || context === UserGrantContext.USER || context === UserGrantContext.NONE">
<span class="role app-label"
*ngFor="let role of grant.roleKeysList">{{ (role.length>6)? (role | slice:0:6)+'..':(role) }}</span>
<button mat-icon-button (click)="getProjectRoleOptions(grant.projectId)"
matTooltip="{{'ACTIONS.CHANGE' | translate}}">
<i class="las la-edit"></i>
</button>
</ng-container>
<mat-form-field class="form-field" appearance="outline" <mat-form-field class="form-field" appearance="outline"
*ngIf="loadedProjectId === grant.projectId"> *ngIf="loadedProjectId && loadedProjectId === grant.projectId">
<mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label> <mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label>
<mat-select [(ngModel)]="grant.roleKeysList" multiple [disabled]="allowWrite == false" <mat-select [(ngModel)]="grant.roleKeysList" multiple
[disabled]="disableWrite || !((['user.grant.write$'] | hasRole | async) || ((context === UserGrantContext.OWNED_PROJECT ? ['user.grant.write:' + grant?.projectId] : context === UserGrantContext.GRANTED_PROJECT ? ['user.grant.write:' + grant?.grantId] : []) | hasRole | async))"
(selectionChange)="updateRoles(grant, $event)"> (selectionChange)="updateRoles(grant, $event)">
<mat-option *ngFor="let role of projectRoleOptions" [value]="role.key"> <mat-option *ngFor="let role of projectRoleOptions" [value]="role.key">
{{role.key}} {{role.key}}
@ -91,10 +97,13 @@
</mat-form-field> </mat-form-field>
</ng-container> </ng-container>
<ng-container *ngIf="context === UserGrantContext.GRANTED_PROJECT"> <ng-container
<mat-form-field class="form-field" appearance="outline"> *ngIf="context === UserGrantContext.GRANTED_PROJECT || context === UserGrantContext.USER || context === UserGrantContext.NONE">
<mat-form-field class="form-field" appearance="outline"
*ngIf="loadedGrantId && loadedGrantId === grant.grantId">
<mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label> <mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label>
<mat-select [(ngModel)]="grant.roleKeysList" multiple [disabled]="allowWrite == false" <mat-select [(ngModel)]="grant.roleKeysList" multiple
[disabled]="disableWrite || !((['user.grant.write$'] | hasRole | async) || ((context === UserGrantContext.OWNED_PROJECT ? ['user.grant.write:' + grant?.projectId] : context === UserGrantContext.GRANTED_PROJECT ? ['user.grant.write:' + grant?.grantId] : []) | hasRole | async))"
(selectionChange)="updateRoles(grant, $event)"> (selectionChange)="updateRoles(grant, $event)">
<mat-option *ngFor="let role of grantRoleOptions" [value]="role"> <mat-option *ngFor="let role of grantRoleOptions" [value]="role">
{{role}} {{role}}
@ -106,11 +115,11 @@
</ng-container> </ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="highlight" mat-row *matRowDef="let row; columns: displayedColumns;"> <tr mat-row *matRowDef="let row; columns: displayedColumns;">
</tr> </tr>
</table> </table>
<mat-paginator class="paginator" #paginator [length]="dataSource.totalResult" [pageSize]="50" <mat-paginator class="paginator" #paginator [length]="dataSource.totalResult" [pageSize]="INITIAL_PAGE_SIZE"
[length]="dataSource.totalResult" [pageSizeOptions]="[2, 3, 25, 50, 100, 250]" (page)="changePage($event)"> [length]="dataSource.totalResult" [pageSizeOptions]="[2, 3, 25, 50, 100, 250]" (page)="changePage($event)">
</mat-paginator> </mat-paginator>
</div> </div>

View File

@ -25,20 +25,38 @@
width: 50px; width: 50px;
max-width: 50px; max-width: 50px;
} }
}
}
.role { .no-roles {
display: inline-block; font-size: 13px;
margin: .25rem; color: var(--grey);
}
.flex-row {
display: flex;
flex-direction: column;
max-width: 400px;
.role {
display: block;
margin: .25rem;
font-size: 14px;
text-overflow: ellipsis;
overflow: hidden;
}
button {
margin: .5rem 0;
max-width: 120px;
i {
font-size: 1.2rem;
margin-bottom: 5px;
} }
} }
} }
.pointer { .fill-space {
outline: none; flex: 1;
cursor: pointer;
}
.no-roles {
font-size: 14px;
color: var(--grey);
} }

View File

@ -4,14 +4,7 @@ import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSelectChange } from '@angular/material/select'; import { MatSelectChange } from '@angular/material/select';
import { MatTable } from '@angular/material/table'; import { MatTable } from '@angular/material/table';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { import { ProjectRoleView, UserGrant, UserGrantView } from 'src/app/proto/generated/management_pb';
ProjectRoleView,
SearchMethod,
UserGrant,
UserGrantSearchKey,
UserGrantSearchQuery,
UserGrantView,
} from 'src/app/proto/generated/management_pb';
import { ManagementService } from 'src/app/services/mgmt.service'; import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@ -23,8 +16,9 @@ import { UserGrantContext, UserGrantsDataSource } from './user-grants-datasource
styleUrls: ['./user-grants.component.scss'], styleUrls: ['./user-grants.component.scss'],
}) })
export class UserGrantsComponent implements OnInit, AfterViewInit { export class UserGrantsComponent implements OnInit, AfterViewInit {
@Input() context: UserGrantContext = UserGrantContext.USER; public INITIAL_PAGE_SIZE: number = 50;
@Input() refreshOnPreviousRoute: string = ''; @Input() context: UserGrantContext = UserGrantContext.NONE;
@Input() refreshOnPreviousRoutes: string[] = [];
public grants: UserGrantView.AsObject[] = []; public grants: UserGrantView.AsObject[] = [];
public dataSource!: UserGrantsDataSource; public dataSource!: UserGrantsDataSource;
@ -32,8 +26,8 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
@ViewChild(MatPaginator) public paginator!: MatPaginator; @ViewChild(MatPaginator) public paginator!: MatPaginator;
@ViewChild(MatTable) public table!: MatTable<UserGrantView.AsObject>; @ViewChild(MatTable) public table!: MatTable<UserGrantView.AsObject>;
@Input() allowWrite: boolean = false; @Input() disableWrite: boolean = false;
@Input() allowDelete: boolean = false; @Input() disableDelete: boolean = false;
@Input() userId: string = ''; @Input() userId: string = '';
@Input() projectId: string = ''; @Input() projectId: string = '';
@ -80,15 +74,11 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
this.routerLink = ['/grant-create', 'user', this.userId]; this.routerLink = ['/grant-create', 'user', this.userId];
} }
break; break;
default: case UserGrantContext.NONE:
this.routerLink = ['/grant-create']; this.routerLink = ['/grant-create'];
} }
this.dataSource.loadGrants(this.context, 0, 25, { this.loadGrantsPage();
projectId: this.projectId,
grantId: this.grantId,
userId: this.userId,
});
} }
public ngAfterViewInit(): void { public ngAfterViewInit(): void {
@ -102,11 +92,12 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
private loadGrantsPage(): void { private loadGrantsPage(): void {
this.dataSource.loadGrants( this.dataSource.loadGrants(
this.context, this.context,
this.paginator.pageIndex, this.paginator?.pageIndex ?? 0,
this.paginator.pageSize, this.paginator?.pageSize ?? this.INITIAL_PAGE_SIZE,
{ {
projectId: this.projectId, projectId: this.projectId,
grantId: this.grantId, grantId: this.grantId,
userId: this.userId,
}, },
); );
} }
@ -125,7 +116,7 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
public getGrantRoleOptions(grantId: string, projectId: string): void { public getGrantRoleOptions(grantId: string, projectId: string): void {
this.mgmtService.GetGrantedProjectByID(projectId, grantId).then(resp => { this.mgmtService.GetGrantedProjectByID(projectId, grantId).then(resp => {
this.loadedGrantId = projectId; this.loadedGrantId = grantId;
this.grantRoleOptions = resp.toObject().roleKeysList; this.grantRoleOptions = resp.toObject().roleKeysList;
}).catch(error => { }).catch(error => {
this.toast.showError(error); this.toast.showError(error);
@ -140,33 +131,12 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
} }
updateRoles(grant: UserGrant.AsObject, selectionChange: MatSelectChange): void { updateRoles(grant: UserGrant.AsObject, selectionChange: MatSelectChange): void {
switch (this.context) { this.userService.UpdateUserGrant(grant.id, grant.userId, selectionChange.value)
case UserGrantContext.OWNED_PROJECT: .then(() => {
if (grant.id && grant.projectId) { this.toast.showInfo('GRANTS.TOAST.UPDATED', true);
this.userService.UpdateUserGrant(grant.id, grant.userId, selectionChange.value) }).catch(error => {
.then(() => { this.toast.showError(error);
this.toast.showInfo('GRANTS.TOAST.UPDATED', true); });
}).catch(error => {
this.toast.showError(error);
});
}
break;
case UserGrantContext.GRANTED_PROJECT:
if (this.grantId && this.projectId) {
const projectQuery: UserGrantSearchQuery = new UserGrantSearchQuery();
projectQuery.setKey(UserGrantSearchKey.USERGRANTSEARCHKEY_PROJECT_ID);
projectQuery.setMethod(SearchMethod.SEARCHMETHOD_EQUALS);
projectQuery.setValue(this.projectId);
this.userService.UpdateUserGrant(
grant.id, grant.userId, selectionChange.value)
.then(() => {
this.toast.showInfo('GRANTS.TOAST.UPDATED', true);
}).catch(error => {
this.toast.showError(error);
});
}
break;
}
} }
deleteGrantSelection(): void { deleteGrantSelection(): void {

View File

@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { GrantsComponent } from './grants.component';
const routes: Routes = [
{
path: '',
component: GrantsComponent,
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class GrantsRoutingModule { }

View File

@ -0,0 +1,9 @@
<div class="max-width-container">
<h2>{{ 'GRANTS.TITLE' | translate }}</h2>
<p class="desc">{{'GRANTS.DESC' | translate }}</p>
<app-user-grants
[displayedColumns]="['select', 'user', 'org', 'projectId', 'creationDate', 'changeDate', 'roleNamesList']"
[disableWrite]="((['user.grant.write$'] | hasRole) | async) == false"
[disableDelete]="((['user.grant.delete$'] | hasRole) | async) == false">
</app-user-grants>
</div>

View File

@ -0,0 +1,4 @@
.desc {
color: var(--grey);
margin-bottom: 2rem;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GrantsComponent } from './grants.component';
describe('GrantsComponent', () => {
let component: GrantsComponent;
let fixture: ComponentFixture<GrantsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [GrantsComponent],
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(GrantsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,8 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-grants',
templateUrl: './grants.component.html',
styleUrls: ['./grants.component.scss'],
})
export class GrantsComponent { }

View File

@ -0,0 +1,24 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { UserGrantsModule } from 'src/app/modules/user-grants/user-grants.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { GrantsRoutingModule } from './grants-routing.module';
import { GrantsComponent } from './grants.component';
@NgModule({
declarations: [
GrantsComponent,
],
imports: [
CommonModule,
GrantsRoutingModule,
UserGrantsModule,
TranslateModule,
HasRoleModule,
HasRolePipeModule,
],
})
export class GrantsModule { }

View File

@ -12,7 +12,7 @@
<span *ngIf="errorMessage" class="err-container">{{errorMessage}}</span> <span *ngIf="errorMessage" class="err-container">{{errorMessage}}</span>
<app-card title="{{ 'APP.PAGES.DETAIL.TITLE' | translate }}" *ngIf="app"> <app-card title="{{ 'APP.PAGES.DETAIL.TITLE' | translate }}" *ngIf="app">
<form [formGroup]="appNameForm" (ngSubmit)="saveOIDCApp()"> <form [formGroup]="appNameForm" (ngSubmit)="saveApp()">
<div class="content"> <div class="content">
<mat-button-toggle-group formControlName="state" class="toggle" (change)="changeState($event)"> <mat-button-toggle-group formControlName="state" class="toggle" (change)="changeState($event)">
<mat-button-toggle [value]="AppState.APPSTATE_INACTIVE" <mat-button-toggle [value]="AppState.APPSTATE_INACTIVE"

View File

@ -216,6 +216,22 @@ export class AppDetailComponent implements OnInit, OnDestroy {
} }
} }
public saveApp(): void {
if (this.appNameForm.valid) {
this.app.name = this.name?.value;
this.mgmtService
.UpdateApplication(this.projectId, this.app.id, this.name?.value)
.then(() => {
this.toast.showInfo('APP.TOAST.OIDCUPDATED', true);
})
.catch(error => {
this.toast.showError(error);
});
}
}
public saveOIDCApp(): void { public saveOIDCApp(): void {
if (this.appNameForm.valid) { if (this.appNameForm.valid) {
this.app.name = this.name?.value; this.app.name = this.name?.value;

View File

@ -18,9 +18,9 @@
<app-user-grants *ngIf="projectId && grantId" [context]="UserGrantContext.GRANTED_PROJECT" <app-user-grants *ngIf="projectId && grantId" [context]="UserGrantContext.GRANTED_PROJECT"
[projectId]="projectId" [grantId]="grantId" [projectId]="projectId" [grantId]="grantId"
[displayedColumns]="['select','user', 'projectId', 'creationDate', 'changeDate', 'roleNamesList']" [displayedColumns]="['select','user', 'projectId', 'creationDate', 'changeDate', 'roleNamesList']"
[allowWrite]="['user.grant.write$','user.grant.write:'+grantId] | hasRole | async" [disableWrite]="(['user.grant.write$','user.grant.write:'+grantId] | hasRole | async) == false"
[allowDelete]="['user.grant.delete$','user.grant.delete:'+grantId] | hasRole | async" [disableDelete]="(['user.grant.delete$','user.grant.delete:'+grantId] | hasRole | async) == false"
refreshOnPreviousRoute="/grant-create/project/{{projectId}}/grant/{{grantId}}"> refreshOnPreviousRoutes="['/grant-create/project/{{projectId}}/grant/{{grantId}}']">
</app-user-grants> </app-user-grants>
</app-card> </app-card>
</ng-template> </ng-template>

View File

@ -68,7 +68,8 @@
[appHasRole]="['project.grant.read:' + project.projectId, 'project.grant.read']"> [appHasRole]="['project.grant.read:' + project.projectId, 'project.grant.read']">
<app-card title="{{ 'PROJECT.GRANT.TITLE' | translate }}" <app-card title="{{ 'PROJECT.GRANT.TITLE' | translate }}"
description="{{ 'PROJECT.GRANT.DESCRIPTION' | translate }}"> description="{{ 'PROJECT.GRANT.DESCRIPTION' | translate }}">
<app-project-grants refreshOnPreviousRoute="/projects/{{projectId}}/grants/create" <app-project-grants
[refreshOnPreviousRoutes]="['/projects/'+projectId+'/grants/create','/projects/'+projectId+'/roles/create']"
[disabled]="((['project.grant.write$', 'project.grant.write:'+ project.projectId]| hasRole | async))== false" [disabled]="((['project.grant.write$', 'project.grant.write:'+ project.projectId]| hasRole | async))== false"
[projectId]="projectId"> [projectId]="projectId">
</app-project-grants> </app-project-grants>
@ -97,9 +98,9 @@
<app-card *ngIf="project?.projectId" title="{{ 'GRANTS.PROJECT.TITLE' | translate }}" <app-card *ngIf="project?.projectId" title="{{ 'GRANTS.PROJECT.TITLE' | translate }}"
description="{{'GRANTS.PROJECT.DESCRIPTION' | translate }}"> description="{{'GRANTS.PROJECT.DESCRIPTION' | translate }}">
<app-user-grants [context]="UserGrantContext.OWNED_PROJECT" [projectId]="projectId" <app-user-grants [context]="UserGrantContext.OWNED_PROJECT" [projectId]="projectId"
refreshOnPreviousRoute="/grant-create/project/{{projectId}}" [refreshOnPreviousRoutes]="['/grant-create/project/'+projectId]"
[allowWrite]="(['user.grant.write$', 'user.grant.write:'+projectId] | hasRole) | async" [disableWrite]="((['user.grant.write$', 'user.grant.write:'+projectId] | hasRole) | async) == false"
[allowDelete]="(['user.grant.delete$','user.grant.delete:'+projectId] | hasRole) | async"> [disableDelete]="((['user.grant.delete$','user.grant.delete:'+projectId] | hasRole) | async) == false">
</app-user-grants> </app-user-grants>
</app-card> </app-card>
</ng-template> </ng-template>

View File

@ -1,6 +1,6 @@
<app-refresh-table [loading]="dataSource?.loading$ | async" *ngIf="projectId" (refreshed)="loadGrantsPage()" <app-refresh-table [loading]="dataSource?.loading$ | async" *ngIf="projectId" (refreshed)="loadGrantsPage()"
[dataSize]="dataSource.totalResult" [selection]="selection" [timestamp]="dataSource?.viewTimestamp" [dataSize]="dataSource.totalResult" [selection]="selection" [timestamp]="dataSource?.viewTimestamp"
[emitRefreshOnPreviousRoute]="refreshOnPreviousRoute"> [emitRefreshOnPreviousRoutes]="refreshOnPreviousRoutes" (refreshed)="getRoleOptions(projectId)">
<ng-template appHasRole [appHasRole]="['project.grant.member.delete:'+projectId, 'project.grant.member.delete']" <ng-template appHasRole [appHasRole]="['project.grant.member.delete:'+projectId, 'project.grant.member.delete']"
actions> actions>
<button (click)="deleteSelectedGrants()" [disabled]="disabled" mat-icon-button *ngIf="selection.hasValue()" <button (click)="deleteSelectedGrants()" [disabled]="disabled" mat-icon-button *ngIf="selection.hasValue()"

View File

@ -24,7 +24,7 @@ import { ProjectGrantsDataSource } from './project-grants-datasource';
], ],
}) })
export class ProjectGrantsComponent implements OnInit, AfterViewInit { export class ProjectGrantsComponent implements OnInit, AfterViewInit {
@Input() refreshOnPreviousRoute: string = ''; @Input() refreshOnPreviousRoutes: string[] = [];
@Input() public projectId: string = ''; @Input() public projectId: string = '';
@Input() public disabled: boolean = false; @Input() public disabled: boolean = false;
@ViewChild(MatPaginator) public paginator!: MatPaginator; @ViewChild(MatPaginator) public paginator!: MatPaginator;
@ -50,7 +50,6 @@ export class ProjectGrantsComponent implements OnInit, AfterViewInit {
tap(() => this.loadGrantsPage()), tap(() => this.loadGrantsPage()),
) )
.subscribe(); .subscribe();
} }
public loadGrantsPage(pageIndex?: number, pageSize?: number): void { public loadGrantsPage(pageIndex?: number, pageSize?: number): void {

View File

@ -16,33 +16,36 @@
{{'PROJECT.GRANT.CREATE.ORG_DESCRIPTION_DESC' | translate}} {{'PROJECT.GRANT.CREATE.ORG_DESCRIPTION_DESC' | translate}}
</p> </p>
<ng-container *ngIf="context && context == UserGrantContext.USER"> <ng-container>
<h1>{{'PROJECT.GRANT.CREATE.SEL_USER' | translate}}</h1>
<app-search-user-autocomplete class="block" singleOutput="true" [users]="user ? [user] : []"
(selectionChanged)="selectUser($event)"
[target]="context === UserGrantContext.USER ? UserTarget.EXTERNAL : UserTarget.SELF">
</app-search-user-autocomplete>
</ng-container>
<ng-container *ngIf="context && (context == UserGrantContext.USER || context == UserGrantContext.NONE)">
<h1>{{'PROJECT.GRANT.CREATE.SEL_PROJECT' | translate}}</h1> <h1>{{'PROJECT.GRANT.CREATE.SEL_PROJECT' | translate}}</h1>
<app-search-project-autocomplete class="block" singleOutput="true" <app-search-project-autocomplete class="block" singleOutput="true"
(selectionChanged)="selectProject($event)"> (selectionChanged)="selectProject($event)">
</app-search-project-autocomplete> </app-search-project-autocomplete>
</ng-container> </ng-container>
<ng-container
*ngIf="context && (context == UserGrantContext.GRANTED_PROJECT || context == UserGrantContext.OWNED_PROJECT)">
<h1>{{'PROJECT.GRANT.CREATE.SEL_USER' | translate}}</h1>
<app-search-user-autocomplete class="block" singleOutput="true" (selectionChanged)="selectUser($event)">
</app-search-user-autocomplete>
</ng-container>
</ng-container> </ng-container>
<ng-container *ngIf="currentCreateStep === STEPS"> <ng-container *ngIf="currentCreateStep === STEPS">
<h1>{{'PROJECT.GRANT.CREATE.SEL_ROLES' | translate}}</h1> <h1>{{'PROJECT.GRANT.CREATE.SEL_ROLES' | translate}}</h1>
<ng-container *ngIf="context === UserGrantContext.OWNED_PROJECT && projectId"> <ng-container
*ngIf="(projectId && (context === UserGrantContext.OWNED_PROJECT || ((context === UserGrantContext.USER || context === UserGrantContext.NONE) && $any(project)?.id == undefined)))">
<app-card> <app-card>
<app-project-roles (changedSelection)="selectRoles($event)" [projectId]="projectId"> <app-project-roles (changedSelection)="selectRoles($event)" [projectId]="projectId">
</app-project-roles> </app-project-roles>
</app-card> </app-card>
</ng-container> </ng-container>
<ng-container *ngIf="context === UserGrantContext.GRANTED_PROJECT && grantRolesKeyList"> <ng-container
*ngIf="(context === UserGrantContext.GRANTED_PROJECT || ((context === UserGrantContext.USER || context === UserGrantContext.NONE) && $any(project)?.id)) && grantRolesKeyList">
<mat-form-field class="form-field" appearance="outline"> <mat-form-field class="form-field" appearance="outline">
<mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label> <mat-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label>
<mat-select multiple (selectionChange)="rolesList = $event.value"> <mat-select multiple (selectionChange)="rolesList = $event.value">
@ -56,9 +59,8 @@
<div class="btn-container"> <div class="btn-container">
<ng-container *ngIf="currentCreateStep === 1"> <ng-container *ngIf="currentCreateStep === 1">
<button <button [disabled]="!org || !projectId || !userId" (click)="next()" color="primary" mat-raised-button
[disabled]="!org || ((context == UserGrantContext.GRANTED_PROJECT || context == UserGrantContext.OWNED_PROJECT) && !projectId) || (context == UserGrantContext.USER && !userId)" class="big-button" cdkFocusInitial>
(click)="next()" color="primary" mat-raised-button class="big-button" cdkFocusInitial>
{{ 'ACTIONS.CONTINUE' | translate }} {{ 'ACTIONS.CONTINUE' | translate }}
</button> </button>
</ng-container> </ng-container>

View File

@ -64,3 +64,13 @@
padding: .5rem 4rem; padding: .5rem 4rem;
} }
} }
.sa-icon {
display: block;
width: 32px;
margin: 0 .5rem;
i {
margin: auto;
}
}

View File

@ -2,6 +2,7 @@ import { Location } from '@angular/common';
import { Component, OnDestroy } from '@angular/core'; import { Component, OnDestroy } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router'; import { ActivatedRoute, Params } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { UserTarget } from 'src/app/modules/search-user-autocomplete/search-user-autocomplete.component';
import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource'; import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource';
import { Org } from 'src/app/proto/generated/auth_pb'; import { Org } from 'src/app/proto/generated/auth_pb';
import { ProjectGrantView, ProjectRole, ProjectView, UserGrant, UserView } from 'src/app/proto/generated/management_pb'; import { ProjectGrantView, ProjectRole, ProjectView, UserGrant, UserView } from 'src/app/proto/generated/management_pb';
@ -19,7 +20,10 @@ export class UserGrantCreateComponent implements OnDestroy {
public org!: Org.AsObject; public org!: Org.AsObject;
public userId: string = ''; public userId: string = '';
public projectId: string = ''; public projectId: string = '';
public project!: ProjectGrantView.AsObject | ProjectView.AsObject;
public grantId: string = ''; public grantId: string = '';
public rolesList: string[] = []; public rolesList: string[] = [];
@ -33,6 +37,12 @@ export class UserGrantCreateComponent implements OnDestroy {
public UserGrantContext: any = UserGrantContext; public UserGrantContext: any = UserGrantContext;
public grantRolesKeyList: string[] = []; public grantRolesKeyList: string[] = [];
public user!: UserView.AsObject;
public UserTarget: any = UserTarget;
public ProjectGrantView: any = ProjectGrantView;
public ProjectView: any = ProjectView;
constructor( constructor(
private userService: ManagementService, private userService: ManagementService,
private toast: ToastService, private toast: ToastService,
@ -42,8 +52,8 @@ export class UserGrantCreateComponent implements OnDestroy {
private mgmtService: ManagementService, private mgmtService: ManagementService,
) { ) {
this.subscription = this.route.params.subscribe((params: Params) => { this.subscription = this.route.params.subscribe((params: Params) => {
const { context, projectid, grantid, userid } = params; const { projectid, grantid, userid } = params;
this.context = context; this.context = UserGrantContext.NONE;
this.projectId = projectid; this.projectId = projectid;
this.grantId = grantid; this.grantId = grantid;
@ -58,6 +68,14 @@ export class UserGrantCreateComponent implements OnDestroy {
}).catch((error: any) => { }).catch((error: any) => {
this.toast.showError(error); this.toast.showError(error);
}); });
} else if (this.userId) {
this.context = UserGrantContext.USER;
this.mgmtService.GetUserByID(this.userId).then(resp => {
this.user = resp.toObject();
console.log(this.user);
}).catch((error: any) => {
this.toast.showError(error);
});
} }
}); });
@ -97,12 +115,52 @@ export class UserGrantCreateComponent implements OnDestroy {
this.toast.showError(error); this.toast.showError(error);
}); });
break; break;
case UserGrantContext.USER:
let grantId;
if ((this.project as ProjectGrantView.AsObject)?.id) {
grantId = (this.project as ProjectGrantView.AsObject).id;
}
this.userService.CreateUserGrant(
this.userId,
this.rolesList,
this.project.projectId,
grantId,
).then((data: UserGrant) => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTUSERGRANTADDED', true);
this.close();
}).catch((error: any) => {
this.toast.showError(error);
});
break;
case UserGrantContext.NONE:
let tempGrantId;
if ((this.project as ProjectGrantView.AsObject)?.id) {
tempGrantId = (this.project as ProjectGrantView.AsObject).id;
}
this.userService.CreateUserGrant(
this.userId,
this.rolesList,
this.project.projectId,
tempGrantId,
).then((data: UserGrant) => {
this.toast.showInfo('PROJECT.GRANT.TOAST.PROJECTGRANTUSERGRANTADDED', true);
this.close();
}).catch((error: any) => {
this.toast.showError(error);
});
break;
} }
} }
public selectProject(project: ProjectView.AsObject | ProjectGrantView.AsObject | any): void { public selectProject(project: ProjectView.AsObject | ProjectGrantView.AsObject | any): void {
this.project = project;
this.projectId = project.projectId; this.projectId = project.projectId;
this.grantRolesKeyList = project.roleKeysList ?? [];
} }
public selectUser(user: UserView.AsObject): void { public selectUser(user: UserView.AsObject): void {

View File

@ -47,6 +47,15 @@
</app-card> </app-card>
<app-auth-user-mfa *ngIf="user" #mfaComponent></app-auth-user-mfa> <app-auth-user-mfa *ngIf="user" #mfaComponent></app-auth-user-mfa>
<app-card *ngIf="user?.id" title="{{ 'GRANTS.USER.TITLE' | translate }}"
description="{{'GRANTS.USER.DESCRIPTION' | translate }}">
<app-user-grants [userId]="user.id" [context]="USERGRANTCONTEXT"
[displayedColumns]="['select', 'projectId', 'creationDate', 'changeDate', 'roleNamesList']"
[disableWrite]="((['user.grant.write$'] | hasRole) | async) == false"
[disableDelete]="((['user.grant.delete$'] | hasRole) | async) == false">
</app-user-grants>
</app-card>
</div> </div>
<div *ngIf="user" class="side" metainfo> <div *ngIf="user" class="side" metainfo>

View File

@ -2,6 +2,7 @@ import { Component, OnDestroy } from '@angular/core';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { ChangeType } from 'src/app/modules/changes/changes.component'; import { ChangeType } from 'src/app/modules/changes/changes.component';
import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource';
import { Gender, UserAddress, UserEmail, UserPhone, UserProfile, UserView } from 'src/app/proto/generated/auth_pb'; import { Gender, UserAddress, UserEmail, UserPhone, UserProfile, UserView } from 'src/app/proto/generated/auth_pb';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@ -26,6 +27,8 @@ export class AuthUserDetailComponent implements OnDestroy {
public ChangeType: any = ChangeType; public ChangeType: any = ChangeType;
public userLoginMustBeDomain: boolean = false; public userLoginMustBeDomain: boolean = false;
public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.USER;
constructor( constructor(
public translate: TranslateService, public translate: TranslateService,
private toast: ToastService, private toast: ToastService,

View File

@ -3,7 +3,7 @@
<span class="label">{{ 'USER.PROFILE.PASSWORD' | translate }}</span> <span class="label">{{ 'USER.PROFILE.PASSWORD' | translate }}</span>
<span>*********</span> <span>*********</span>
<div> <div class="overflow">
<ng-content select="[phoneAction]"></ng-content> <ng-content select="[phoneAction]"></ng-content>
<a [disabled]="!canWrite" [routerLink]="['password']" mat-icon-button> <a [disabled]="!canWrite" [routerLink]="['password']" mat-icon-button>
<mat-icon class="icon">chevron_right</mat-icon> <mat-icon class="icon">chevron_right</mat-icon>

View File

@ -22,7 +22,7 @@
.label { .label {
font-size: .9rem; font-size: .9rem;
min-width: 100px; max-width: 100px;
color: var(--grey); color: var(--grey);
} }
@ -45,3 +45,7 @@
} }
} }
} }
.overflow {
overflow: auto;
}

View File

@ -83,10 +83,10 @@
<app-card *ngIf="user?.id" title="{{ 'GRANTS.USER.TITLE' | translate }}" <app-card *ngIf="user?.id" title="{{ 'GRANTS.USER.TITLE' | translate }}"
description="{{'GRANTS.USER.DESCRIPTION' | translate }}"> description="{{'GRANTS.USER.DESCRIPTION' | translate }}">
<app-user-grants [userId]="user.id" <app-user-grants [userId]="user.id" [context]="USERGRANTCONTEXT"
[allowWrite]="['user.grant.write$'+ 'user.grant.write:'+user?.id] | hasRole | async"
[displayedColumns]="['select', 'projectId', 'creationDate', 'changeDate', 'roleNamesList']" [displayedColumns]="['select', 'projectId', 'creationDate', 'changeDate', 'roleNamesList']"
[allowDelete]="['user.grant.delete$', 'user.grant.delete'+ user?.id] | hasRole | async"> [disableWrite]="((['user.grant.write$'] | hasRole) | async) == false"
[disableDelete]="((['user.grant.delete$'] | hasRole) | async) == false">
</app-user-grants> </app-user-grants>
</app-card> </app-card>
</div> </div>

View File

@ -5,6 +5,7 @@ import { ActivatedRoute } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { ChangeType } from 'src/app/modules/changes/changes.component'; import { ChangeType } from 'src/app/modules/changes/changes.component';
import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component'; import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { import {
Gender, Gender,
@ -37,6 +38,7 @@ export class UserDetailComponent implements OnInit, OnDestroy {
public UserState: any = UserState; public UserState: any = UserState;
public copied: string = ''; public copied: string = '';
public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.USER;
constructor( constructor(
public translate: TranslateService, public translate: TranslateService,

View File

@ -1,6 +1,6 @@
<app-refresh-table [loading]="loading$ | async" (refreshed)="refreshPage()" [dataSize]="dataSource.data.length" <app-refresh-table [loading]="loading$ | async" (refreshed)="refreshPage()" [dataSize]="dataSource.data.length"
[timestamp]="userResult?.viewTimestamp" [selection]="selection" [timestamp]="userResult?.viewTimestamp" [selection]="selection"
[emitRefreshOnPreviousRoute]="refreshOnPreviousRoute"> [emitRefreshOnPreviousRoutes]="refreshOnPreviousRoutes">
<mat-form-field @appearfade *ngIf="userSearchKey != undefined" actions class="filtername"> <mat-form-field @appearfade *ngIf="userSearchKey != undefined" actions class="filtername">
<mat-label>{{'USER.PAGES.FILTER' | translate}}</mat-label> <mat-label>{{'USER.PAGES.FILTER' | translate}}</mat-label>
<input matInput (keyup)="applyFilter($event)" <input matInput (keyup)="applyFilter($event)"

View File

@ -27,7 +27,7 @@ export class UserTableComponent implements OnInit {
public userSearchKey: UserSearchKey | undefined = undefined; public userSearchKey: UserSearchKey | undefined = undefined;
public UserType: any = UserType; public UserType: any = UserType;
@Input() userType: UserType = UserType.HUMAN; @Input() userType: UserType = UserType.HUMAN;
@Input() refreshOnPreviousRoute: string = ''; @Input() refreshOnPreviousRoutes: string[] = [];
@Input() disabled: boolean = false; @Input() disabled: boolean = false;
@ViewChild(MatPaginator) public paginator!: MatPaginator; @ViewChild(MatPaginator) public paginator!: MatPaginator;
@ViewChild('input') public filter!: MatInput; @ViewChild('input') public filter!: MatInput;

View File

@ -1,19 +1,16 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
import { AuthenticationService } from './authentication.service';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class ToastService { export class ToastService {
constructor(private dialog: MatDialog, constructor(
private snackBar: MatSnackBar, private snackBar: MatSnackBar,
private translate: TranslateService, private translate: TranslateService,
private authService: AuthenticationService,
) { } ) { }
public showInfo(message: string, i18nkey: boolean = false): void { public showInfo(message: string, i18nkey: boolean = false): void {
@ -21,17 +18,23 @@ export class ToastService {
this.translate this.translate
.get(message) .get(message)
.subscribe(data => { .subscribe(data => {
this.showMessage(data, 'close'); this.translate.get('ACTIONS.CLOSE').pipe(take(1)).subscribe(value => {
this.showMessage(data, value);
});
}); });
} else { } else {
this.showMessage(message, 'close'); this.translate.get('ACTIONS.CLOSE').pipe(take(1)).subscribe(value => {
this.showMessage(message, value);
});
} }
} }
public showError(grpcError: any): void { public showError(grpcError: any): void {
const { message, code, metadata } = grpcError; const { message, code, metadata } = grpcError;
if (code !== 16) { if (code !== 16) {
this.showMessage(decodeURI(message), 'close'); this.translate.get('ACTIONS.CLOSE').pipe(take(1)).subscribe(value => {
this.showMessage(decodeURI(message), value);
});
} }
} }

View File

@ -35,7 +35,9 @@
"LOGOUT": "Alle Benutzer abmelden", "LOGOUT": "Alle Benutzer abmelden",
"NEWORG":"Neue Organisation", "NEWORG":"Neue Organisation",
"IAMADMIN":"Du bist ein IAM-Administrator. Beachte, dass Du erhöhte Rechte besitzt.", "IAMADMIN":"Du bist ein IAM-Administrator. Beachte, dass Du erhöhte Rechte besitzt.",
"SHOWORGS":"Alle Organisationen anzeigen" "SHOWORGS":"Alle Organisationen anzeigen",
"GRANTSECTION":"Berechtigungssektion",
"GRANTS":"Berechtigungen"
}, },
"ACTIONS": { "ACTIONS": {
"SAVE": "Speichern", "SAVE": "Speichern",
@ -870,6 +872,8 @@
}, },
"ROLESLABEL":"Rollen", "ROLESLABEL":"Rollen",
"GRANTS": { "GRANTS": {
"TITLE":"Berechtigungen",
"DESC":"Hier kannst Du die Berechtigungen Deiner Organisation verwalten.",
"DELETE":"Berechtigung löschen", "DELETE":"Berechtigung löschen",
"ADD":"Berechtigung erstellen", "ADD":"Berechtigung erstellen",
"ADD_BTN":"Neu", "ADD_BTN":"Neu",

View File

@ -35,7 +35,9 @@
"LOGOUT": "Logout All Users", "LOGOUT": "Logout All Users",
"NEWORG":"New Organisation", "NEWORG":"New Organisation",
"IAMADMIN":"You are an IAM Administrator. Note that you have extended permissions.", "IAMADMIN":"You are an IAM Administrator. Note that you have extended permissions.",
"SHOWORGS":"Show All Organisations" "SHOWORGS":"Show All Organisations",
"GRANTSECTION":"Authorization Section",
"GRANTS":"Authorizations"
}, },
"ACTIONS": { "ACTIONS": {
"SAVE": "Save", "SAVE": "Save",
@ -870,6 +872,8 @@
}, },
"ROLESLABEL":"Roles", "ROLESLABEL":"Roles",
"GRANTS": { "GRANTS": {
"TITLE":"Authorisations",
"DESC":"Here you can manage authorizations of your organization users.",
"DELETE":"Delete Authorisation", "DELETE":"Delete Authorisation",
"ADD":"Create Authorisation", "ADD":"Create Authorisation",
"ADD_BTN":"New", "ADD_BTN":"New",

View File

@ -31,9 +31,9 @@
} }
tr { tr {
cursor: pointer;
&.highlight { &.highlight {
cursor: pointer;
&:hover { &:hover {
td { td {
background-color: var(--table-row-back); // rgba($inv-color, .05); background-color: var(--table-row-back); // rgba($inv-color, .05);