feat(console): identity providers and login policies (#722)

* idp list, idp create route

* idp modules, lazy import, i18n, routing

* generic service, i18n

* seperate lockout, age policy component

* seperate component modules

* routing

* enum class

* login policy

* iam policy grid

* login policy providers

* idps login policy

* add idp dialog component

* add idp to loginpolicy

* delete idp config, iam policy grid

* remove idp from loginpolicy

* idp detail component, generic idp create

* lint

* idp detail clientid-secrets, issuer, scopes

* hide clientsecret on update

* rm background style, idp config

* app tooltip fix

* lint

* dont refresh on idp select

* Update console/src/app/modules/idp-create/idp-create.component.html

Co-authored-by: Fabiennne <[email protected]>
Co-authored-by: Fabi <[email protected]>
This commit is contained in:
Max Peintner
2020-09-17 10:22:41 +02:00
committed by GitHub
co-authored by Fabiennne Fabi
parent 845026e43f
commit 58b01cdf3f
96 changed files with 3401 additions and 735 deletions
@@ -0,0 +1,85 @@
<app-refresh-table [loading]="loading$ | async" (refreshed)="refreshPage()" [dataSize]="dataSource.data.length"
[timestamp]="idpResult?.viewTimestamp" [selection]="selection">
<ng-template appHasRole [appHasRole]="['iam.write']" actions>
<button (click)="deactivateSelectedIdps()" matTooltip="{{'IDP.DEACTIVATE' | translate}}" class="icon-button"
mat-icon-button *ngIf="selection.hasValue()" [disabled]="disabled">
<mat-icon svgIcon="mdi_account_cancel"></mat-icon>
</button>
<button (click)="reactivateSelectedIdps()" matTooltip="{{'IDP.ACTIVATE' | translate}}" class="icon-button"
mat-icon-button *ngIf="selection.hasValue()" [disabled]="disabled">
<mat-icon svgIcon="mdi_account_check_outline"></mat-icon>
</button>
<button color="warn" (click)="removeSelectedIdps()" matTooltip="{{'IDP.DELETE' | translate}}"
class="icon-button" mat-icon-button *ngIf="selection.hasValue()" [disabled]="disabled">
<i class="las la-trash"></i>
</button>
<a class="add-button" [routerLink]="createRouterLink" color="primary" mat-raised-button [disabled]="disabled">
<mat-icon class="icon">add</mat-icon>{{ 'ACTIONS.NEW' | translate }}
</a>
</ng-template>
<div class="table-wrapper">
<table class="table" mat-table [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef>
<mat-checkbox color="primary" (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td mat-cell *matCellDef="let idp">
<mat-checkbox color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(idp) : null" [checked]="selection.isSelected(idp)">
<img *ngIf="idp?.logoSrc?.startsWith('https://'); else genAvatar" [src]="idp.logoSrc"
alt="ipp logo {{idp?.name}}" />
<ng-template #genAvatar>
<div class="avatar">
<span>{{idp.name.charAt(0)}}</span>
</div>
</ng-template>
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'IDP.NAME' | translate }} </th>
<td mat-cell *matCellDef="let idp"> {{idp?.name}} </td>
</ng-container>
<ng-container matColumnDef="config">
<th mat-header-cell *matHeaderCellDef> {{ 'IDP.CONFIG' | translate }} </th>
<td mat-cell *matCellDef="let idp">
<div *ngFor="let elem of idp?.oidcConfig | keyvalue" class="flex-row">
<span class="key">{{elem.key}}:</span>
<span class="value">{{elem.value}}</span>
</div>
</td>
</ng-container>
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> {{ 'IDP.STATE' | translate }} </th>
<td mat-cell *matCellDef="let idp"> {{ 'IDP.STATES.'+idp.state | translate }} </td>
</ng-container>
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{ 'IDP.CREATIONDATE' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let idp">
{{idp.creationDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }} </td>
</ng-container>
<ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef> {{ 'IDP.CHANGEDATE' | translate }} </th>
<td class="pointer" mat-cell *matCellDef="let idp">
{{idp.changeDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr class="data-row" mat-row *matRowDef="let row; columns: displayedColumns;"
[routerLink]="row.id ? [serviceType == PolicyComponentServiceType.ADMIN ? '/iam' : '/org', 'idp', row.id ]: null">
</tr>
</table>
<mat-paginator #paginator class="paginator" [length]="idpResult?.totalResult || 0" [pageSize]="10"
[pageSizeOptions]="[5, 10, 20]" (page)="changePage($event)"></mat-paginator>
</div>
</app-refresh-table>
@@ -0,0 +1,77 @@
.table-wrapper {
overflow: auto;
.table,
.paginator {
width: 100%;
td,
th {
padding: 0 1rem;
&:first-child {
padding-left: 0;
padding-right: 1rem;
}
&:last-child {
padding-right: 0;
}
}
.data-row {
cursor: pointer;
&:hover {
background-color: #ffffff05;
}
}
}
}
tr {
outline: none;
}
.add-button {
border-radius: .5rem;
}
.avatar {
height: 30px;
width: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: black;
color: white;
span {
font-size: 14px;
text-transform: uppercase;
}
}
.flex-row {
display: flex;
justify-content: space-between;
.key {
margin-right: 1rem;
font-size: 14px;
}
.value {
font-size: 14px;
}
&:first-child {
padding-top: .5rem;
}
&:last-child {
padding-bottom: .5rem;
}
}
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { IdpTableComponent } from './idp-table.component';
describe('UserTableComponent', () => {
let component: IdpTableComponent;
let fixture: ComponentFixture<IdpTableComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [IdpTableComponent],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(IdpTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,126 @@
import { SelectionModel } from '@angular/cdk/collections';
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { RouterLink } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { IdpSearchResponse as AdminIdpSearchResponse, IdpView as AdminIdpView } from 'src/app/proto/generated/admin_pb';
import { IdpView as MgmtIdpView } from 'src/app/proto/generated/management_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentServiceType } from '../policies/policy-component-types.enum';
@Component({
selector: 'app-idp-table',
templateUrl: './idp-table.component.html',
styleUrls: ['./idp-table.component.scss'],
})
export class IdpTableComponent implements OnInit {
@Input() public serviceType!: PolicyComponentServiceType;
@Input() service!: AdminService | ManagementService;
@Input() disabled: boolean = false;
@ViewChild(MatPaginator) public paginator!: MatPaginator;
public dataSource: MatTableDataSource<AdminIdpView.AsObject | MgmtIdpView.AsObject>
= new MatTableDataSource<AdminIdpView.AsObject | MgmtIdpView.AsObject>();
public selection: SelectionModel<AdminIdpView.AsObject | MgmtIdpView.AsObject>
= new SelectionModel<AdminIdpView.AsObject | MgmtIdpView.AsObject>(true, []);
public idpResult!: AdminIdpSearchResponse.AsObject;
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public PolicyComponentServiceType: any = PolicyComponentServiceType;
@Input() public displayedColumns: string[] = ['select', 'name', 'config', 'creationDate', 'changeDate', 'state'];
@Output() public changedSelection: EventEmitter<Array<AdminIdpView.AsObject | MgmtIdpView.AsObject>>
= new EventEmitter();
constructor(public translate: TranslateService, private toast: ToastService) {
this.selection.changed.subscribe(() => {
this.changedSelection.emit(this.selection.selected);
});
}
ngOnInit(): void {
this.getData(10, 0);
}
public isAllSelected(): boolean {
const numSelected = this.selection.selected.length;
const numRows = this.dataSource.data.length;
return numSelected === numRows;
}
public masterToggle(): void {
this.isAllSelected() ?
this.selection.clear() :
this.dataSource.data.forEach(row => this.selection.select(row));
}
public changePage(event: PageEvent): void {
this.getData(event.pageSize, event.pageIndex * event.pageSize);
}
public deactivateSelectedIdps(): void {
Promise.all(this.selection.selected.map(value => {
return this.service.DeactivateIdpConfig(value.id);
})).then(() => {
this.toast.showInfo('USER.TOAST.SELECTEDDEACTIVATED', true);
this.getData(10, 0);
});
}
public reactivateSelectedIdps(): void {
Promise.all(this.selection.selected.map(value => {
return this.service.ReactivateIdpConfig(value.id);
})).then(() => {
this.toast.showInfo('USER.TOAST.SELECTEDREACTIVATED', true);
this.getData(10, 0);
});
}
public removeSelectedIdps(): void {
Promise.all(this.selection.selected.map(value => {
return this.service.RemoveIdpConfig(value.id);
})).then(() => {
this.toast.showInfo('USER.TOAST.SELECTEDDEACTIVATED', true);
this.getData(10, 0);
});
}
private async getData(limit: number, offset: number): Promise<void> {
this.loadingSubject.next(true);
// let query: AdminIdpSearchQuery | MgmtIdpSearchQuery;
// if (this.service instanceof AdminService) {
// query = new AdminIdpSearchQuery();
// query.setKey(AdminIdpSearchKey.)
// } else if (this.service instanceof ManagementService) {
// return ['/org', 'idp', 'create'];
// }
this.service.SearchIdps(limit, offset).then(resp => {
this.idpResult = resp.toObject();
this.dataSource.data = this.idpResult.resultList;
console.log(this.idpResult.resultList);
this.loadingSubject.next(false);
}).catch(error => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
}
public refreshPage(): void {
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize);
}
public get createRouterLink(): RouterLink | any {
if (this.service instanceof AdminService) {
return ['/iam', 'idp', 'create'];
} else if (this.service instanceof ManagementService) {
return ['/org', 'idp', 'create'];
}
}
}
@@ -0,0 +1,42 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { RefreshTableModule } from 'src/app/modules/refresh-table/refresh-table.module';
import { LocalizedDatePipeModule } from 'src/app/pipes/localized-date-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe.module';
import { IdpTableComponent } from './idp-table.component';
@NgModule({
declarations: [IdpTableComponent],
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
MatButtonModule,
MatCheckboxModule,
MatIconModule,
MatTooltipModule,
TranslateModule,
LocalizedDatePipeModule,
TimestampToDatePipeModule,
MatTableModule,
MatPaginatorModule,
RouterModule,
RefreshTableModule,
HasRoleModule,
],
exports: [
IdpTableComponent,
],
})
export class IdpTableModule { }