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
41 changed files with 483 additions and 178 deletions

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>
<app-card title="{{ 'APP.PAGES.DETAIL.TITLE' | translate }}" *ngIf="app">
<form [formGroup]="appNameForm" (ngSubmit)="saveOIDCApp()">
<form [formGroup]="appNameForm" (ngSubmit)="saveApp()">
<div class="content">
<mat-button-toggle-group formControlName="state" class="toggle" (change)="changeState($event)">
<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 {
if (this.appNameForm.valid) {
this.app.name = this.name?.value;

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
<app-refresh-table [loading]="dataSource?.loading$ | async" *ngIf="projectId" (refreshed)="loadGrantsPage()"
[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']"
actions>
<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 {
@Input() refreshOnPreviousRoute: string = '';
@Input() refreshOnPreviousRoutes: string[] = [];
@Input() public projectId: string = '';
@Input() public disabled: boolean = false;
@ViewChild(MatPaginator) public paginator!: MatPaginator;
@@ -50,7 +50,6 @@ export class ProjectGrantsComponent implements OnInit, AfterViewInit {
tap(() => this.loadGrantsPage()),
)
.subscribe();
}
public loadGrantsPage(pageIndex?: number, pageSize?: number): void {

View File

@@ -16,33 +16,36 @@
{{'PROJECT.GRANT.CREATE.ORG_DESCRIPTION_DESC' | translate}}
</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>
<app-search-project-autocomplete class="block" singleOutput="true"
(selectionChanged)="selectProject($event)">
</app-search-project-autocomplete>
</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 *ngIf="currentCreateStep === STEPS">
<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-project-roles (changedSelection)="selectRoles($event)" [projectId]="projectId">
</app-project-roles>
</app-card>
</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-label>{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}</mat-label>
<mat-select multiple (selectionChange)="rolesList = $event.value">
@@ -56,9 +59,8 @@
<div class="btn-container">
<ng-container *ngIf="currentCreateStep === 1">
<button
[disabled]="!org || ((context == UserGrantContext.GRANTED_PROJECT || context == UserGrantContext.OWNED_PROJECT) && !projectId) || (context == UserGrantContext.USER && !userId)"
(click)="next()" color="primary" mat-raised-button class="big-button" cdkFocusInitial>
<button [disabled]="!org || !projectId || !userId" (click)="next()" color="primary" mat-raised-button
class="big-button" cdkFocusInitial>
{{ 'ACTIONS.CONTINUE' | translate }}
</button>
</ng-container>

View File

@@ -64,3 +64,13 @@
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 { ActivatedRoute, Params } from '@angular/router';
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 { Org } from 'src/app/proto/generated/auth_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 userId: string = '';
public projectId: string = '';
public project!: ProjectGrantView.AsObject | ProjectView.AsObject;
public grantId: string = '';
public rolesList: string[] = [];
@@ -33,6 +37,12 @@ export class UserGrantCreateComponent implements OnDestroy {
public UserGrantContext: any = UserGrantContext;
public grantRolesKeyList: string[] = [];
public user!: UserView.AsObject;
public UserTarget: any = UserTarget;
public ProjectGrantView: any = ProjectGrantView;
public ProjectView: any = ProjectView;
constructor(
private userService: ManagementService,
private toast: ToastService,
@@ -42,8 +52,8 @@ export class UserGrantCreateComponent implements OnDestroy {
private mgmtService: ManagementService,
) {
this.subscription = this.route.params.subscribe((params: Params) => {
const { context, projectid, grantid, userid } = params;
this.context = context;
const { projectid, grantid, userid } = params;
this.context = UserGrantContext.NONE;
this.projectId = projectid;
this.grantId = grantid;
@@ -58,6 +68,14 @@ export class UserGrantCreateComponent implements OnDestroy {
}).catch((error: any) => {
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);
});
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 {
this.project = project;
this.projectId = project.projectId;
this.grantRolesKeyList = project.roleKeysList ?? [];
}
public selectUser(user: UserView.AsObject): void {

View File

@@ -47,6 +47,15 @@
</app-card>
<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 *ngIf="user" class="side" metainfo>

View File

@@ -2,6 +2,7 @@ import { Component, OnDestroy } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs';
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 { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service';
@@ -26,6 +27,8 @@ export class AuthUserDetailComponent implements OnDestroy {
public ChangeType: any = ChangeType;
public userLoginMustBeDomain: boolean = false;
public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.USER;
constructor(
public translate: TranslateService,
private toast: ToastService,

View File

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

View File

@@ -22,7 +22,7 @@
.label {
font-size: .9rem;
min-width: 100px;
max-width: 100px;
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 }}"
description="{{'GRANTS.USER.DESCRIPTION' | translate }}">
<app-user-grants [userId]="user.id"
[allowWrite]="['user.grant.write$'+ 'user.grant.write:'+user?.id] | hasRole | async"
<app-user-grants [userId]="user.id" [context]="USERGRANTCONTEXT"
[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-card>
</div>

View File

@@ -5,6 +5,7 @@ import { ActivatedRoute } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs';
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 {
Gender,
@@ -37,6 +38,7 @@ export class UserDetailComponent implements OnInit, OnDestroy {
public UserState: any = UserState;
public copied: string = '';
public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.USER;
constructor(
public translate: TranslateService,

View File

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

View File

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