feat(console): JWT IDP, cleanup login policy, update deps (#2438)

* idp cleanup

* lint

* jwtidp service, create, detail, assets

* idp detail, info row

* detail actions

* delete idp, fix state change

* lint

* cli core

* cdk material

* chore(deps-dev): bump karma-jasmine-html-reporter in /console (#2446)

Bumps [karma-jasmine-html-reporter](https://github.com/dfederm/karma-jasmine-html-reporter) from 1.6.0 to 1.7.0.
- [Release notes](https://github.com/dfederm/karma-jasmine-html-reporter/releases)
- [Commits](https://github.com/dfederm/karma-jasmine-html-reporter/compare/v1.6.0...v1.7.0)

---
updated-dependencies:
- dependency-name: karma-jasmine-html-reporter
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* update deps

* lock

* disable actions, user grant link to user, add granted org desc

* lint

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Max Peintner
2021-10-21 06:29:13 +00:00
committed by GitHub
co-authored by dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
parent dbec8e164d
commit 1c20ea5a50
67 changed files with 2571 additions and 1947 deletions
@@ -1,42 +1,42 @@
<div class="card" appOutsideClick (clickOutside)="closeCard($event)">
<app-avatar
*ngIf="user.human?.profile && (user.human?.profile?.displayName || (user.human?.profile?.firstName && user.human?.profile?.lastName))"
class="avatar"
[forColor]="user.preferredLoginName" [avatarUrl]="user.human?.profile?.avatarUrl || ''"
[name]="user.human?.profile?.displayName ? user.human?.profile?.displayName : (user.human?.profile?.firstName + ' '+ user.human?.profile?.lastName)"
[size]="80">
</app-avatar>
<app-avatar
*ngIf="user.human?.profile && (user.human?.profile?.displayName || (user.human?.profile?.firstName && user.human?.profile?.lastName))"
class="avatar" [forColor]="user.preferredLoginName" [avatarUrl]="user.human?.profile?.avatarUrl || ''"
[name]="user.human?.profile?.displayName ? user.human?.profile?.displayName : (user.human?.profile?.firstName + ' '+ user.human?.profile?.lastName)"
[size]="80">
</app-avatar>
<span class="u-name">{{user.human?.profile?.displayName ? user.human?.profile?.displayName : 'A'}}</span>
<span class="u-email">{{user.human?.profile?.preferredLoginName}}</span>
<span class="iamuser" *ngIf="iamuser">IAM USER</span>
<span class="u-name">{{user.human?.profile?.displayName ? user.human?.profile?.displayName : 'A'}}</span>
<span class="u-email">{{user?.preferredLoginName}}</span>
<span class="iamuser" *ngIf="iamuser">IAM USER</span>
<button color="primary" (click)="editUserProfile()" mat-stroked-button>{{'USER.EDITACCOUNT' | translate}}</button>
<div class="l-accounts">
<mat-progress-bar *ngIf="loadingUsers" color="primary" mode="indeterminate"></mat-progress-bar>
<a class="row" *ngFor="let session of sessions" (click)="selectAccount(session.loginName)">
<app-avatar *ngIf="session && session.displayName" class="small-avatar" [avatarUrl]="session.avatarUrl || ''" [forColor]="session.loginName" [size]="32">
</app-avatar>
<button color="primary" (click)="editUserProfile()" mat-stroked-button>{{'USER.EDITACCOUNT' | translate}}</button>
<div class="l-accounts">
<mat-progress-bar *ngIf="loadingUsers" color="primary" mode="indeterminate"></mat-progress-bar>
<a class="row" *ngFor="let session of sessions" (click)="selectAccount(session.loginName)">
<app-avatar *ngIf="session && session.displayName" class="small-avatar" [avatarUrl]="session.avatarUrl || ''"
[forColor]="session.loginName" [size]="32">
</app-avatar>
<div class="col">
<span class="user-title">{{session.displayName ? session.displayName : session.userName}} </span>
<span class="loginname">{{session.loginName}}</span>
<span class="email">{{'USER.STATE.'+session.authState | translate}}</span>
</div>
<span class="fill-space"></span>
<mat-icon>keyboard_arrow_right</mat-icon>
</a>
<a class="row" (click)="selectNewAccount()">
<div class="icon-wrapper">
<i class="las la-user-plus"></i>
</div>
<span class="col">
<span class="user-title">{{'USER.ADDACCOUNT' | translate}}</span>
</span>
<span class="fill-space"></span>
<mat-icon>keyboard_arrow_right</mat-icon>
</a>
</div>
<div class="col">
<span class="user-title">{{session.displayName ? session.displayName : session.userName}} </span>
<span class="loginname">{{session.loginName}}</span>
<span class="email">{{'USER.STATE.'+session.authState | translate}}</span>
</div>
<span class="fill-space"></span>
<mat-icon>keyboard_arrow_right</mat-icon>
</a>
<a class="row" (click)="selectNewAccount()">
<div class="icon-wrapper">
<i class="las la-user-plus"></i>
</div>
<span class="col">
<span class="user-title">{{'USER.ADDACCOUNT' | translate}}</span>
</span>
<span class="fill-space"></span>
<mat-icon>keyboard_arrow_right</mat-icon>
</a>
</div>
<button (click)="logout()" color="primary" mat-stroked-button>{{'MENU.LOGOUT' | translate}}</button>
<button (click)="logout()" color="primary" mat-stroked-button>{{'MENU.LOGOUT' | translate}}</button>
</div>
@@ -1,6 +1,6 @@
<div class="radio-button-wrapper">
<ng-container *ngFor="let type of types">
<input type="radio" (change)="emitChange()" [value]="type" [(ngModel)]="selected" [id]="type.prefix" />
<input class="app" type="radio" (change)="emitChange()" [value]="type" [(ngModel)]="selected" [id]="type.prefix" />
<label class="cnsl-type-radio-button" [for]="type.prefix">
<div class="cnsl-type-radio-header" [ngStyle]="{'background': type.background}">
<span>{{type.prefix}}</span>
@@ -12,13 +12,13 @@
$primary-color: mat.get-color-from-palette($primary, 500);
$is-dark-theme: map-get($theme, is-dark);
input[type="radio"] {
input[type="radio"].app {
appearance: none;
opacity: 0;
display: none;
}
input:checked + label {
input.app:checked + label {
border-color: if($is-dark-theme, white, var(--grey));
.cnsl-type-radio-header span {
@@ -1,45 +1,43 @@
<div class="groups">
<span class="co-header">{{ title }}</span>
<span class="sub-header">{{ description }} {{'MEMBER.DOCSINFO' | translate}} <a
href="https://docs.zitadel.ch/docs/manuals/admin-managers" target="_blank">ZITADEL Managers</a>.
</span>
<div class="people">
<div class="img-list" [@cardAnimation]="totalResult">
<mat-spinner class="spinner" diameter="20" *ngIf="loading"></mat-spinner>
<span class="co-header">{{ title }}</span>
<span class="sub-header">{{ description }} {{'MEMBER.DOCSINFO' | translate}} <a
href="https://docs.zitadel.ch/docs/manuals/admin-managers" target="_blank">ZITADEL Managers</a>.
</span>
<div class="people">
<div class="img-list" [@cardAnimation]="totalResult">
<mat-spinner class="spinner" diameter="20" *ngIf="loading"></mat-spinner>
<ng-container *ngIf="totalResult < 10; else compact">
<ng-container *ngFor="let member of membersSubject | async; index as i">
<div @animate (click)="emitShowDetail()" class="avatar-circle"
matTooltip="{{ member.displayName }} | {{member.rolesList?.join(' ')}}"
[ngStyle]="{'z-index': 100 - i}">
<app-avatar
*ngIf="member && member.displayName && member.firstName && member.lastName; else cog"
class="avatar dontcloseonclick" [avatarUrl]="member.avatarUrl|| ''" [forColor]="member?.userName"[forColor]="member?.preferredLoginName"
[name]="member.displayName ? member.displayName : (member.firstName + ' '+ member.lastName)"
[size]="32">
</app-avatar>
<ng-template #cog>
<div class="sa-icon">
<i class="las la-user-cog"></i>
</div>
</ng-template>
</div>
</ng-container>
</ng-container>
<ng-template #compact>
<div (click)="emitShowDetail()" class="avatar-circle" matTooltip="Click to show detail">
<span>{{totalResult}}</span>
</div>
<ng-container *ngIf="totalResult < 10; else compact">
<ng-container *ngFor="let member of membersSubject | async; index as i">
<div @animate (click)="emitShowDetail()" class="avatar-circle"
matTooltip="{{ member.displayName }} | {{member.rolesList?.join(' ')}}" [ngStyle]="{'z-index': 100 - i}">
<app-avatar *ngIf="member && member.displayName && member.firstName && member.lastName; else cog"
class="avatar dontcloseonclick" [avatarUrl]="member.avatarUrl|| ''" [forColor]="member?.userName"
[forColor]="member?.preferredLoginName"
[name]="member.displayName ? member.displayName : (member.firstName + ' '+ member.lastName)" [size]="32">
</app-avatar>
<ng-template #cog>
<div class="sa-icon">
<i class="las la-user-cog"></i>
</div>
</ng-template>
<button class="add-img" (click)="emitAddMember()" [disabled]="disabled" mat-icon-button
matTooltip="{{'ACTIONS.ADD' | translate}}" aria-label="Edit contributors">
<mat-icon>add</mat-icon>
</button>
<span class="fill-space"></span>
<button matTooltip="{{'ACTIONS.REFRESH' | translate}}" class="refresh-img" (click)="emitRefresh()"
[disabled]="disabled" mat-icon-button aria-label="refresh contributors">
<mat-icon class="icon">refresh</mat-icon>
</button>
</div>
</ng-container>
</ng-container>
<ng-template #compact>
<div (click)="emitShowDetail()" class="avatar-circle" matTooltip="Click to show detail">
<span>{{totalResult}}</span>
</div>
</ng-template>
<button class="add-img" (click)="emitAddMember()" [disabled]="disabled" mat-icon-button
matTooltip="{{'ACTIONS.ADD' | translate}}" aria-label="Edit contributors">
<mat-icon>add</mat-icon>
</button>
<span class="fill-space"></span>
<button matTooltip="{{'ACTIONS.REFRESH' | translate}}" class="refresh-img" (click)="emitRefresh()" mat-icon-button
aria-label="refresh contributors">
<mat-icon class="icon">refresh</mat-icon>
</button>
</div>
</div>
</div>
@@ -7,9 +7,16 @@
</div>
<div class="detail-right">
<div class="head">
<h1>{{ title }}</h1>
<p class="desc">{{ description }}</p>
<ng-content></ng-content>
<div class="top-view">
<div>
<h1>{{ title }}</h1>
<p class="head-desc">{{ description }}</p>
</div>
<div class="actions-wrap">
<ng-content select="[actions]"></ng-content>
</div>
</div>
<ng-content></ng-content>
</div>
</div>
</div>
@@ -46,15 +46,26 @@
.head {
margin-bottom: 2rem;
h1 {
font-size: 1.5rem;
margin-top: 10px;
.top-view {
display: flex;
justify-content: space-between;
}
.desc {
display: block;
font-size: .9rem;
color: var(--grey);
div {
h1 {
font-size: 1.5rem;
margin-top: 10px;
}
.head-desc {
display: block;
font-size: .9rem;
color: var(--grey);
}
}
.actions-wrap {
padding-top: .5rem;
}
}
}
@@ -9,78 +9,147 @@
</div>
<h1>{{'IDP.CREATE.TITLE' | translate}}</h1>
<p>{{'IDP.CREATE.DESCRIPTION' | translate}}</p>
<mat-progress-bar *ngIf="loading" color="primary" mode="indeterminate"></mat-progress-bar>
<form (ngSubmit)="addIdp()">
<ng-container [formGroup]="formGroup">
<div class="content">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="name" />
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.ISSUER' | translate }}</cnsl-label>
<input cnslInput formControlName="issuer" />
</cnsl-form-field>
</div>
<ng-container *ngIf="currentCreateStep === 1">
<p class="desc">{{'IDP.CREATE.DESCRIPTION' | translate}}</p>
<cnsl-info-section class="auto-reg-info">
<div>
<p class="auto-reg-desc">{{'IDP.AUTOREGISTER_DESC' | translate}}</p>
<mat-checkbox formControlName="autoRegister">
{{'IDP.AUTOREGISTER' | translate}}
</mat-checkbox>
</div>
</cnsl-info-section>
<cnsl-idp-type-radio [types]="idpTypes" (selectedType)="idpType = $event"
[selected]="idpType"></cnsl-idp-type-radio>
<div class="content">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.CLIENTID' | translate }}</cnsl-label>
<input cnslInput formControlName="clientId" />
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.CLIENTSECRET' | translate }}</cnsl-label>
<input cnslInput formControlName="clientSecret" />
</cnsl-form-field>
</div>
<div class="content">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.SCOPESLIST' | translate }}</cnsl-label>
<mat-chip-list #chipScopesList aria-label="scope selection" *ngIf="scopesList">
<mat-chip class="chip" *ngFor="let scope of scopesList.value" selectable="false" removable
(removed)="removeScope(scope)">
{{scope}} <mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input cnslInput [matChipInputFor]="chipScopesList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" [matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addScope($event)">
</mat-chip-list>
</cnsl-form-field>
</div>
<div class="content">
<cnsl-form-field class="formfield" appearance="outline">
<cnsl-label>{{ 'IDP.IDPDISPLAYNAMMAPPING' | translate }}</cnsl-label>
<mat-select formControlName="idpDisplayNameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINGFIELD.'+field | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field class="formfield" appearance="outline">
<cnsl-label>{{ 'IDP.USERNAMEMAPPING' | translate }}</cnsl-label>
<mat-select formControlName="usernameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINGFIELD.'+field | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
</div>
</ng-container>
<div class="actions">
<button mat-raised-button [disabled]="!idpType" color="primary"
(click)="currentCreateStep = 2">{{'ACTIONS.CONTINUE' | translate}}</button>
</div>
</ng-container>
<button color="primary" mat-raised-button class="continue-button" [disabled]="formGroup.invalid" type="submit">
<ng-container *ngIf="currentCreateStep === 2 && idpType === OIDC">
<p class="desc">{{'IDP.OIDC.DESCRIPTION' | translate}}</p>
<form [formGroup]="oidcFormGroup" (ngSubmit)="addOIDCIdp()">
<div class="idp-content">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="name" />
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.ISSUER' | translate }}</cnsl-label>
<input cnslInput formControlName="issuer" />
</cnsl-form-field>
</div>
<cnsl-info-section class="auto-reg-info">
<div>
<p class="auto-reg-desc">{{'IDP.AUTOREGISTER_DESC' | translate}}</p>
<mat-checkbox formControlName="autoRegister">
{{'IDP.AUTOREGISTER' | translate}}
</mat-checkbox>
</div>
</cnsl-info-section>
<div class="idp-content">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.CLIENTID' | translate }}</cnsl-label>
<input cnslInput formControlName="clientId" />
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.CLIENTSECRET' | translate }}</cnsl-label>
<input cnslInput formControlName="clientSecret" />
</cnsl-form-field>
</div>
<div class="idp-content">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.SCOPESLIST' | translate }}</cnsl-label>
<mat-chip-list #chipScopesList aria-label="scope selection" *ngIf="scopesList">
<mat-chip class="chip" *ngFor="let scope of scopesList.value" selectable="false" removable
(removed)="removeScope(scope)">
{{scope}} <mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input cnslInput [matChipInputFor]="chipScopesList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" [matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addScope($event)">
</mat-chip-list>
</cnsl-form-field>
</div>
<div class="idp-content">
<cnsl-form-field class="formfield" appearance="outline">
<cnsl-label>{{ 'IDP.IDPDISPLAYNAMMAPPING' | translate }}</cnsl-label>
<mat-select formControlName="idpDisplayNameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINGFIELD.'+field | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field class="formfield" appearance="outline">
<cnsl-label>{{ 'IDP.USERNAMEMAPPING' | translate }}</cnsl-label>
<mat-select formControlName="usernameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINGFIELD.'+field | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
</div>
<div class="actions">
<button color="primary" (click)="currentCreateStep = 1" mat-stroked-button class="back-button" type="button">
{{ 'ACTIONS.BACK' | translate }}
</button>
<button color="primary" mat-raised-button class="continue-button" [disabled]="oidcFormGroup.invalid" type="submit">
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</form>
</ng-container>
<ng-container *ngIf="currentCreateStep === 2 && idpType === JWT">
<p class="desc">{{'IDP.JWT.DESCRIPTION' | translate}}</p>
<form [formGroup]="jwtFormGroup" (ngSubmit)="addJWTIdp()">
<div class="idp-content">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="jwtName" />
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.JWT.HEADERNAME' | translate }}</cnsl-label>
<input cnslInput formControlName="jwtHeaderName" />
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.ISSUER' | translate }}</cnsl-label>
<input cnslInput formControlName="jwtIssuer" />
</cnsl-form-field>
</div>
<cnsl-info-section class="auto-reg-info">
<div>
<p class="auto-reg-desc">{{'IDP.AUTOREGISTER_DESC' | translate}}</p>
<mat-checkbox formControlName="jwtAutoRegister">
{{'IDP.AUTOREGISTER' | translate}}
</mat-checkbox>
</div>
</cnsl-info-section>
<div class="idp-content">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.JWT.JWTENDPOINT' | translate }}</cnsl-label>
<input cnslInput formControlName="jwtEndpoint" />
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.JWT.JWTKEYSENDPOINT' | translate }}</cnsl-label>
<input cnslInput formControlName="jwtKeysEndpoint" />
</cnsl-form-field>
</div>
<div class="actions">
<button color="primary" (click)="currentCreateStep = 1" mat-stroked-button class="back-button" type="button">
{{ 'ACTIONS.BACK' | translate }}
</button>
<button color="primary" mat-raised-button class="continue-button" [disabled]="jwtFormGroup.invalid" type="submit">
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</form>
</ng-container>
</div>
@@ -1,3 +1,7 @@
.desc {
color: var(--grey);
font-size: 14px;
}
.container {
padding: 4rem 4rem 2rem 4rem;
@@ -29,7 +33,6 @@
}
.auto-reg-info {
margin: 0 .5rem 1rem .5rem;
display: block;
width: 100%;
@@ -38,7 +41,7 @@
}
}
.content {
.idp-content {
display: flex;
margin: 0 -.5rem;
flex-wrap: wrap;
@@ -51,7 +54,7 @@
}
.formfield {
flex: 1 0 auto;
flex: 1;
margin: 0 .5rem;
@media only screen and (max-width: 450px) {
@@ -60,13 +63,17 @@
}
}
.continue-button {
margin-top: 3rem;
display: block;
padding: .5rem 4rem;
@media only screen and (max-width: 450px) {
.actions {
button[mat-stroked-button] {
float: left;
margin-top: 1rem;
margin-bottom: 2rem;
border-radius: .5rem;
}
button[mat-raised-button] {
float: right;
margin-top: 1rem;
border-radius: .5rem;
min-width: 100px;
}
}
@@ -6,14 +6,15 @@ import { MatChipInputEvent } from '@angular/material/chips';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { AddOIDCIDPRequest } from 'src/app/proto/generated/zitadel/admin_pb';
import { AddJWTIDPRequest, AddOIDCIDPRequest } from 'src/app/proto/generated/zitadel/admin_pb';
import { OIDCMappingField } from 'src/app/proto/generated/zitadel/idp_pb';
import { AddOrgOIDCIDPRequest } from 'src/app/proto/generated/zitadel/management_pb';
import { AddOrgJWTIDPRequest, AddOrgOIDCIDPRequest } from 'src/app/proto/generated/zitadel/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';
import { JWT, OIDC, RadioItemIdpType } from './idptypes';
@Component({
selector: 'app-idp-create',
@@ -29,10 +30,23 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
private subscription?: Subscription;
public projectId: string = '';
public formGroup!: FormGroup;
public createSteps: number = 1;
public oidcFormGroup!: FormGroup;
public jwtFormGroup!: FormGroup;
public createSteps: number = 2;
public currentCreateStep: number = 1;
public loading: boolean = false;
public idpTypes: RadioItemIdpType[] = [
OIDC,
JWT,
];
OIDC: any = OIDC;
JWT: any = JWT;
public idpType!: RadioItemIdpType;
constructor(
private router: Router,
private route: ActivatedRoute,
@@ -40,7 +54,7 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
private injector: Injector,
private _location: Location,
) {
this.formGroup = new FormGroup({
this.oidcFormGroup = new FormGroup({
name: new FormControl('', [Validators.required]),
clientId: new FormControl('', [Validators.required]),
clientSecret: new FormControl('', [Validators.required]),
@@ -51,6 +65,16 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
autoRegister: new FormControl(false),
});
this.jwtFormGroup = new FormGroup({
jwtName: new FormControl('', [Validators.required]),
jwtHeaderName: new FormControl('', [Validators.required]),
jwtIssuer: new FormControl('', [Validators.required]),
jwtEndpoint: new FormControl('', [Validators.required]),
jwtKeysEndpoint: new FormControl('', [Validators.required]),
jwtStylingType: new FormControl(0),
jwtAutoRegister: new FormControl(false),
});
this.route.data.pipe(take(1)).subscribe(data => {
this.serviceType = data.serviceType;
switch (this.serviceType) {
@@ -82,7 +106,7 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
this.projectId = projectid;
}
public addIdp(): void {
public addOIDCIdp(): void {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
const req = new AddOrgOIDCIDPRequest();
@@ -133,6 +157,56 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
}
}
public addJWTIdp(): void {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
const req = new AddOrgJWTIDPRequest();
req.setName(this.jwtName?.value);
req.setHeaderName(this.jwtHeaderName?.value);
req.setIssuer(this.jwtIssuer?.value);
req.setJwtEndpoint(this.jwtEndpoint?.value);
req.setKeysEndpoint(this.jwtKeysEndpoint?.value);
req.setAutoRegister(this.jwtAutoRegister?.value);
req.setStylingType(this.jwtStylingType?.value);
this.loading = true;
(this.service as ManagementService).addOrgJWTIDP(req).then((idp) => {
setTimeout(() => {
this.loading = false;
this.router.navigate([
(this.serviceType === PolicyComponentServiceType.MGMT ? 'org' :
this.serviceType === PolicyComponentServiceType.ADMIN ? 'iam' : ''),
'policy', 'login']);
}, 2000);
}).catch(error => {
this.toast.showError(error);
});
} else if (PolicyComponentServiceType.ADMIN) {
const req = new AddJWTIDPRequest();
req.setName(this.jwtName?.value);
req.setHeaderName(this.jwtHeaderName?.value);
req.setIssuer(this.jwtIssuer?.value);
req.setJwtEndpoint(this.jwtEndpoint?.value);
req.setKeysEndpoint(this.jwtKeysEndpoint?.value);
req.setAutoRegister(this.jwtAutoRegister?.value);
req.setStylingType(this.jwtStylingType?.value);
this.loading = true;
(this.service as AdminService).addJWTIDP(req).then((idp) => {
setTimeout(() => {
this.loading = false;
this.router.navigate([
(this.serviceType === PolicyComponentServiceType.MGMT ? 'org' :
this.serviceType === PolicyComponentServiceType.ADMIN ? 'iam' : ''),
'policy', 'login']);
}, 2000);
}).catch(error => {
this.toast.showError(error);
});
}
}
public close(): void {
this._location.back();
}
@@ -162,35 +236,62 @@ export class IdpCreateComponent implements OnInit, OnDestroy {
}
public get name(): AbstractControl | null {
return this.formGroup.get('name');
return this.oidcFormGroup.get('name');
}
public get clientId(): AbstractControl | null {
return this.formGroup.get('clientId');
return this.oidcFormGroup.get('clientId');
}
public get clientSecret(): AbstractControl | null {
return this.formGroup.get('clientSecret');
return this.oidcFormGroup.get('clientSecret');
}
public get issuer(): AbstractControl | null {
return this.formGroup.get('issuer');
return this.oidcFormGroup.get('issuer');
}
public get scopesList(): AbstractControl | null {
return this.formGroup.get('scopesList');
return this.oidcFormGroup.get('scopesList');
}
public get autoRegister(): AbstractControl | null {
return this.formGroup.get('autoRegister');
return this.oidcFormGroup.get('autoRegister');
}
public get idpDisplayNameMapping(): AbstractControl | null {
return this.formGroup.get('idpDisplayNameMapping');
return this.oidcFormGroup.get('idpDisplayNameMapping');
}
public get usernameMapping(): AbstractControl | null {
return this.formGroup.get('usernameMapping');
return this.oidcFormGroup.get('usernameMapping');
}
public get jwtName(): AbstractControl | null {
return this.jwtFormGroup.get('jwtName');
}
public get jwtHeaderName(): AbstractControl | null {
return this.jwtFormGroup.get('jwtHeaderName');
}
public get jwtIssuer(): AbstractControl | null {
return this.jwtFormGroup.get('jwtIssuer');
}
public get jwtEndpoint(): AbstractControl | null {
return this.jwtFormGroup.get('jwtEndpoint');
}
public get jwtKeysEndpoint(): AbstractControl | null {
return this.jwtFormGroup.get('jwtKeysEndpoint');
}
public get jwtStylingType(): AbstractControl | null {
return this.jwtFormGroup.get('jwtStylingType');
}
public get jwtAutoRegister(): AbstractControl | null {
return this.jwtFormGroup.get('jwtAutoRegister');
}
}
@@ -14,9 +14,10 @@ import { InputModule } from 'src/app/modules/input/input.module';
import { InfoSectionModule } from '../info-section/info-section.module';
import { IdpCreateRoutingModule } from './idp-create-routing.module';
import { IdpCreateComponent } from './idp-create.component';
import { IdpTypeRadioComponent } from './idp-type-radio/idp-type-radio.component';
@NgModule({
declarations: [IdpCreateComponent],
declarations: [IdpCreateComponent, IdpTypeRadioComponent],
imports: [
IdpCreateRoutingModule,
CommonModule,
@@ -0,0 +1,12 @@
<div class="idp-radio-button-wrapper">
<ng-container *ngFor="let type of types">
<input class="idp mat-elevation-z1" type="radio" (change)="emitChange()" [value]="type" [(ngModel)]="selected" [id]="type.titleI18nKey" />
<label class="cnsl-idp-type-radio-button" [for]="type.titleI18nKey">
<div class="cnsl-idp-type-radio-header" >
<mat-icon class="icon" *ngIf="type.mdi" [svgIcon]="type.mdi"></mat-icon>
<span class="fill-space"></span>
<span>{{type.titleI18nKey | translate}}</span>
</div>
</label>
</ng-container>
</div>
@@ -0,0 +1,60 @@
@use '~@angular/material' as mat;
.idp-radio-button-wrapper {
display: flex;
flex-direction: column;
flex-wrap: wrap;
margin: 0 -.5rem;
max-width: 500px;
}
@mixin idp-type-radio-theme($theme) {
$primary: map-get($theme, primary);
$primary-color: mat.get-color-from-palette($primary, 500);
$primary-color-light: mat.get-color-from-palette($primary, 300);
$primary-color-dark: mat.get-color-from-palette($primary, 700);
$primary-color-contrast: mat.get-color-from-palette($primary, default-contrast);
$is-dark-theme: map-get($theme, is-dark);
$back: map-get($theme, background);
$fg: map-get($theme, foreground);
$cardback: map-get($back, card);
$text: map-get($fg, text);
input[type="radio"].idp {
appearance: none;
opacity: 0;
display: none;
}
.cnsl-idp-type-radio-button {
margin: .5rem;
border-radius: .5rem;
display: flex;
flex-direction: column;
cursor: pointer;
position: relative;
box-shadow: inset 0 0 6px rgba(0, 0, 0, .1);
background-color: $cardback;
.cnsl-idp-type-radio-header {
display: flex;
align-items: center;
padding: 0 1rem;
span {
margin: 1rem 0;
font-size: 1.1rem;
}
.fill-space {
flex: 1;
}
}
}
input.idp:checked + label {
border: 4px solid if($is-dark-theme, $primary-color-dark, $primary-color-light) !important;
background: $primary-color !important;
color: $primary-color-contrast !important;
}
}
@@ -0,0 +1,18 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { OIDC, RadioItemIdpType } from '../idptypes';
@Component({
selector: 'cnsl-idp-type-radio',
templateUrl: './idp-type-radio.component.html',
styleUrls: ['./idp-type-radio.component.scss'],
})
export class IdpTypeRadioComponent {
@Input() selected: RadioItemIdpType = OIDC;
@Input() types!: RadioItemIdpType[];
@Output() selectedType: EventEmitter<RadioItemIdpType> = new EventEmitter();
public emitChange(): void {
this.selectedType.emit(this.selected);
}
}
@@ -0,0 +1,23 @@
export enum IdpCreateType {
OIDC = 'OIDC',
JWT = 'JWT',
}
export interface RadioItemIdpType {
createType: IdpCreateType;
titleI18nKey: string;
mdi?: string;
}
export const OIDC = {
titleI18nKey: 'IDP.OIDC.TITLE',
mdi: 'mdi_openid',
createType: IdpCreateType.OIDC,
};
export const JWT = {
titleI18nKey: 'IDP.JWT.TITLE',
mdi: 'mdi_jwt',
createType: IdpCreateType.JWT,
};
@@ -38,8 +38,14 @@
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> {{ 'IDP.NAME' | translate }} </th>
<td [routerLink]="routerLinkForRow(idp)" mat-cell *matCellDef="let idp"><strong>{{idp?.name}}</strong>
</td>
<td [routerLink]="routerLinkForRow(idp)" mat-cell *matCellDef="let idp">
<div class="name-col">
<span>{{idp?.name}}</span>
<mat-icon matTooltip="{{'IDP.OIDC.TITLE' | translate}}" *ngIf="idp?.oidcConfig" svgIcon="mdi_openid"></mat-icon>
<mat-icon matTooltip="{{'IDP.JWT.TITLE' | translate}}" *ngIf="idp?.jwtConfig" svgIcon="mdi_jwt"></mat-icon>
</div>
</td>
</ng-container>
<ng-container matColumnDef="config">
@@ -62,7 +68,7 @@
</ng-container>
<ng-container matColumnDef="dates">
<th mat-header-cell *matHeaderCellDef> {{ 'IDP.CREATIONDATE' | translate }} </th>
<th mat-header-cell *matHeaderCellDef> {{ 'IDP.DATES' | translate }} </th>
<td [routerLink]="routerLinkForRow(idp)" class="pointer" mat-cell *matCellDef="let idp">
<div class="date-block">
<span class="date-sub">{{ 'IDP.CREATIONDATE' | translate }}:</span>
@@ -76,7 +82,7 @@
</ng-container>
<ng-container matColumnDef="owner">
<th mat-header-cell *matHeaderCellDef> {{ 'IDP.TYPE' | translate }} </th>
<th mat-header-cell *matHeaderCellDef> {{ 'IDP.OWNER' | translate }} </th>
<td [routerLink]="routerLinkForRow(idp)" class="pointer" mat-cell *matCellDef="let idp">
{{'IDP.OWNERTYPES.'+idp.owner | translate }} </td>
</ng-container>
@@ -88,7 +94,7 @@
[disabled]="serviceType==PolicyComponentServiceType.MGMT && idp?.providerType == IDPOwnerType.IDP_OWNER_TYPE_ORG"
mat-icon-button color="warn" matTooltip="{{'ACTIONS.REMOVE' | translate}}"
(click)="removeIdp(idp)">
<mat-icon>remove_circle</mat-icon>
<i class="las la-trash"></i>
</button>
</td>
</ng-container>
@@ -8,6 +8,8 @@
.table,
.paginator {
width: 100% !important;
td,
th {
padding: 0 1rem;
@@ -35,6 +37,15 @@ td {
object-fit: contain;
margin-top: .5rem;
}
.name-col {
display: flex;
flex-direction: column;
span {
margin-bottom: .5rem;
}
}
}
tr {
@@ -6,8 +6,8 @@ import { RouterLink } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { ListIDPsResponse } from 'src/app/proto/generated/zitadel/admin_pb';
import { IDP, IDPOwnerType, IDPState, IDPStylingType } from 'src/app/proto/generated/zitadel/idp_pb';
import { ListOrgIDPsResponse } from 'src/app/proto/generated/zitadel/management_pb';
import { IDP, IDPOwnerType, IDPOwnerTypeQuery, IDPState, IDPStylingType } from 'src/app/proto/generated/zitadel/idp_pb';
import { IDPQuery, ListOrgIDPsResponse } from 'src/app/proto/generated/zitadel/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';
@@ -17,187 +17,190 @@ import { PolicyComponentServiceType } from '../policies/policy-component-types.e
import { WarnDialogComponent } from '../warn-dialog/warn-dialog.component';
@Component({
selector: 'app-idp-table',
templateUrl: './idp-table.component.html',
styleUrls: ['./idp-table.component.scss'],
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(PaginatorComponent) public paginator!: PaginatorComponent;
public dataSource: MatTableDataSource<IDP.AsObject>
= new MatTableDataSource<IDP.AsObject>();
public selection: SelectionModel<IDP.AsObject>
= new SelectionModel<IDP.AsObject>(true, []);
public idpResult!: ListIDPsResponse.AsObject | ListOrgIDPsResponse.AsObject;
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public PolicyComponentServiceType: any = PolicyComponentServiceType;
public IDPOwnerType: any = IDPOwnerType;
public IDPState: any = IDPState;
public IDPSTYLINGTYPE: any = IDPStylingType;
@Input() public displayedColumns: string[] = ['select', 'name', 'config', 'dates', 'state'];
@Input() public serviceType!: PolicyComponentServiceType;
@Input() service!: AdminService | ManagementService;
@Input() disabled: boolean = false;
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
public dataSource: MatTableDataSource<IDP.AsObject>
= new MatTableDataSource<IDP.AsObject>();
public selection: SelectionModel<IDP.AsObject>
= new SelectionModel<IDP.AsObject>(true, []);
public idpResult!: ListIDPsResponse.AsObject | ListOrgIDPsResponse.AsObject;
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public PolicyComponentServiceType: any = PolicyComponentServiceType;
public IDPOwnerType: any = IDPOwnerType;
public IDPState: any = IDPState;
public IDPSTYLINGTYPE: any = IDPStylingType;
@Input() public displayedColumns: string[] = ['select', 'name', 'dates', 'state'];
@Output() public changedSelection: EventEmitter<Array<IDP.AsObject>>
= new EventEmitter();
@Output() public changedSelection: EventEmitter<Array<IDP.AsObject>>
= new EventEmitter();
constructor(public translate: TranslateService, private toast: ToastService, private dialog: MatDialog) {
this.selection.changed.subscribe(() => {
this.changedSelection.emit(this.selection.selected);
});
constructor(public translate: TranslateService, private toast: ToastService, private dialog: MatDialog) {
this.selection.changed.subscribe(() => {
this.changedSelection.emit(this.selection.selected);
});
}
ngOnInit(): void {
this.getData(10, 0);
if (this.serviceType === PolicyComponentServiceType.MGMT) {
this.displayedColumns = ['select', 'name', 'dates', 'state', 'owner'];
}
ngOnInit(): void {
this.getData(10, 0);
if (!this.disabled) {
this.displayedColumns.push('actions');
}
}
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 {
const map: Promise<any>[] = this.selection.selected.map(value => {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
return (this.service as ManagementService).deactivateOrgIDP(value.id);
} else {
return (this.service as AdminService).deactivateIDP(value.id);
}
});
Promise.all(map).then(() => {
this.selection.clear();
this.toast.showInfo('IDP.TOAST.SELECTEDDEACTIVATED', true);
this.refreshPage();
}).catch(error => {
this.toast.showError(error);
});
}
public reactivateSelectedIdps(): void {
const map: Promise<any>[] = this.selection.selected.map(value => {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
return (this.service as ManagementService).reactivateOrgIDP(value.id);
} else {
return (this.service as AdminService).reactivateIDP(value.id);
}
});
Promise.all(map).then(() => {
this.selection.clear();
this.toast.showInfo('IDP.TOAST.SELECTEDREACTIVATED', true);
this.refreshPage();
}).catch(error => {
this.toast.showError(error);
});
}
public removeIdp(idp: IDP.AsObject): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'IDP.DELETE_TITLE',
descriptionKey: 'IDP.DELETE_DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
this.displayedColumns = ['select', 'name', 'config', 'dates', 'state', 'owner'];
}
if (!this.disabled) {
this.displayedColumns.push('actions');
}
}
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 {
const map: Promise<any>[] = this.selection.selected.map(value => {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
return (this.service as ManagementService).deactivateOrgIDP(value.id);
} else {
return (this.service as AdminService).deactivateIDP(value.id);
}
});
Promise.all(map).then(() => {
this.selection.clear();
this.toast.showInfo('IDP.TOAST.SELECTEDDEACTIVATED', true);
this.refreshPage();
}).catch(error => {
(this.service as ManagementService).removeOrgIDP(idp.id).then(() => {
this.toast.showInfo('IDP.TOAST.DELETED', true);
setTimeout(() => {
this.refreshPage();
}, 1000);
}, error => {
this.toast.showError(error);
});
}
public reactivateSelectedIdps(): void {
const map: Promise<any>[] = this.selection.selected.map(value => {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
return (this.service as ManagementService).reactivateOrgIDP(value.id);
} else {
return (this.service as AdminService).reactivateIDP(value.id);
}
});
Promise.all(map).then(() => {
this.selection.clear();
this.toast.showInfo('IDP.TOAST.SELECTEDREACTIVATED', true);
this.refreshPage();
}).catch(error => {
this.toast.showError(error);
});
}
public removeIdp(idp: IDP.AsObject): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'IDP.DELETE_TITLE',
descriptionKey: 'IDP.DELETE_DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
(this.service as ManagementService).removeOrgIDP(idp.id).then(() => {
this.toast.showInfo('IDP.TOAST.DELETED', true);
setTimeout(() => {
this.refreshPage();
}, 1000);
}, error => {
this.toast.showError(error);
});
} else {
(this.service as AdminService).removeIDP(idp.id).then(() => {
this.toast.showInfo('IDP.TOAST.DELETED', true);
setTimeout(() => {
this.refreshPage();
}, 1000);
}, error => {
this.toast.showError(error);
});
}
}
});
}
private async getData(limit: number, offset: number): Promise<void> {
this.loadingSubject.next(true);
if (this.serviceType === PolicyComponentServiceType.MGMT) {
(this.service as ManagementService).listOrgIDPs(limit, offset).then(resp => {
this.idpResult = resp;
this.dataSource.data = resp.resultList;
this.loadingSubject.next(false);
}).catch(error => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
});
} else {
(this.service as AdminService).listIDPs(limit, offset).then(resp => {
this.idpResult = resp;
this.dataSource.data = resp.resultList;
this.loadingSubject.next(false);
}).catch(error => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
(this.service as AdminService).removeIDP(idp.id).then(() => {
this.toast.showInfo('IDP.TOAST.DELETED', true);
setTimeout(() => {
this.refreshPage();
}, 1000);
}, error => {
this.toast.showError(error);
});
}
}
});
}
private async getData(limit: number, offset: number): Promise<void> {
this.loadingSubject.next(true);
if (this.serviceType === PolicyComponentServiceType.MGMT) {
const query: IDPQuery = new IDPQuery();
const otQuery: IDPOwnerTypeQuery = new IDPOwnerTypeQuery();
otQuery.setOwnerType(IDPOwnerType.IDP_OWNER_TYPE_ORG);
query.setOwnerTypeQuery(otQuery);
(this.service as ManagementService).listOrgIDPs(limit, offset, [query]).then(resp => {
this.idpResult = resp;
this.dataSource.data = resp.resultList;
this.loadingSubject.next(false);
}).catch(error => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
} else {
(this.service as AdminService).listIDPs(limit, offset).then(resp => {
this.idpResult = resp;
this.dataSource.data = resp.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'];
}
}
public refreshPage(): void {
this.getData(this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize);
}
public routerLinkForRow(row: IDP.AsObject): any {
if (row.id) {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
switch (row.owner) {
case IDPOwnerType.IDP_OWNER_TYPE_SYSTEM:
return ['/iam', 'idp', row.id];
case IDPOwnerType.IDP_OWNER_TYPE_ORG:
return ['/org', 'idp', row.id];
}
break;
case PolicyComponentServiceType.ADMIN:
return ['/iam', 'idp', row.id];
}
}
public get createRouterLink(): RouterLink | any {
if (this.service instanceof AdminService) {
return ['/iam', 'idp', 'create'];
} else if (this.service instanceof ManagementService) {
return ['/org', 'idp', 'create'];
}
}
public routerLinkForRow(row: IDP.AsObject): any {
if (row.id) {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
switch (row.owner) {
case IDPOwnerType.IDP_OWNER_TYPE_SYSTEM:
return ['/iam', 'idp', row.id];
case IDPOwnerType.IDP_OWNER_TYPE_ORG:
return ['/org', 'idp', row.id];
}
break;
case PolicyComponentServiceType.ADMIN:
return ['/iam', 'idp', row.id];
}
}
}
}
+134 -74
View File
@@ -1,13 +1,35 @@
<app-detail-layout [backRouterLink]="backroutes" [title]="'IDP.DETAIL.TITLE' | translate"
<app-detail-layout [backRouterLink]="backroutes" [title]="idp?.name"
[description]="'IDP.DETAIL.DESCRIPTION' | translate">
<div *ngIf="canWrite | async" actions>
<button class="actions-trigger" mat-raised-button color="primary" [matMenuTriggerFor]="idpactions">
<span>{{'ACTIONS.ACTIONS' | translate}}</span>
<mat-icon class="icon">keyboard_arrow_down</mat-icon>
</button>
<mat-menu #idpactions="matMenu" xPosition="before">
<button mat-menu-item
*ngIf="idp?.state !== IDPState.IDP_STATE_INACTIVE"
(click)="changeState(IDPState.IDP_STATE_INACTIVE)">
{{'ACTIONS.DEACTIVATE' | translate}}
</button>
<button mat-menu-item *ngIf="idp?.state == IDPState.IDP_STATE_INACTIVE"
(click)="changeState(IDPState.IDP_STATE_ACTIVE)">
{{'ACTIONS.REACTIVATE' | translate}}
</button>
<button mat-menu-item matTooltip="{{'IDP.DELETE' | translate}}"
(click)="deleteIdp()">
<span [style.color]="'var(--warn)'">{{'IDP.DELETE_TITLE' | translate}}</span>
</button>
</mat-menu>
</div>
<div class="container">
<form (ngSubmit)="updateIdp()">
<cnsl-info-row *ngIf="idp" [idp]="idp"></cnsl-info-row>
<form class="idp-form" (ngSubmit)="updateIdp()">
<ng-container [formGroup]="idpForm">
<div class="content">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.ID' | translate }}</cnsl-label>
<input cnslInput formControlName="id" />
</cnsl-form-field>
<div class="idp-content">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="name" />
@@ -40,77 +62,115 @@
</div>
</form>
<ng-container *ngIf="oidcConfigForm">
<h2>{{'IDP.DETAIL.OIDC.TITLE' | translate}}</h2>
<p>{{'IDP.DETAIL.OIDC.DESCRIPTION' | translate}}</p>
<ng-container *ngIf="idp?.oidcConfig && oidcConfigForm">
<h2>{{'IDP.OIDC.TITLE' | translate}}</h2>
<p>{{'IDP.OIDC.DESCRIPTION' | translate}}</p>
<form (ngSubmit)="updateOidcConfig()">
<ng-container [formGroup]="oidcConfigForm">
<div class="content">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.ISSUER' | translate }}</cnsl-label>
<input cnslInput formControlName="issuer" />
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.CLIENTID' | translate }}</cnsl-label>
<input cnslInput formControlName="clientId" />
</cnsl-form-field>
<mat-checkbox class="desc" [(ngModel)]="showIdSecretSection" [disabled]="(canWrite | async) === false"
[ngModelOptions]="{standalone: true}">
Update Client Secret
</mat-checkbox>
<cnsl-form-field appearance="outline" class="formfield" *ngIf="showIdSecretSection">
<cnsl-label>{{ 'IDP.CLIENTSECRET' | translate }}</cnsl-label>
<input cnslInput formControlName="clientSecret" />
</cnsl-form-field>
<div class="line">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.SCOPESLIST' | translate }}</cnsl-label>
<form (ngSubmit)="updateOidcConfig()">
<ng-container [formGroup]="oidcConfigForm">
<div class="idp-content">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.ISSUER' | translate }}</cnsl-label>
<input cnslInput formControlName="issuer" />
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.CLIENTID' | translate }}</cnsl-label>
<input cnslInput formControlName="clientId" />
</cnsl-form-field>
<mat-checkbox class="idp-desc" [(ngModel)]="showIdSecretSection" [disabled]="(canWrite | async) === false"
[ngModelOptions]="{standalone: true}">
Update Client Secret
</mat-checkbox>
<cnsl-form-field appearance="outline" class="formfield" *ngIf="showIdSecretSection">
<cnsl-label>{{ 'IDP.CLIENTSECRET' | translate }}</cnsl-label>
<input cnslInput formControlName="clientSecret" />
</cnsl-form-field>
<div class="line">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.SCOPESLIST' | translate }}</cnsl-label>
<input cnslInput [matChipInputFor]="chipScopesList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" [matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addScope($event)">
</cnsl-form-field>
<button (click)="addScope($event)" mat-icon-button>
<mat-icon>add</mat-icon>
</button>
</div>
<cnsl-form-field appearance="outline" class="formfield fullwidth">
<mat-chip-list class="chip-list" #chipScopesList aria-label="scope selection">
<mat-chip class="chip" *ngFor="let scope of scopesList?.value" selectable="false"
removable (removed)="removeScope(scope)" [disabled]="(canWrite | async) === false">
{{scope}} <mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
</mat-chip-list>
</cnsl-form-field>
<cnsl-form-field class="formfield" appearance="outline">
<cnsl-label>{{ 'IDP.IDPDISPLAYNAMMAPPING' | translate }}</cnsl-label>
<mat-select formControlName="displayNameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINGFIELD.'+field | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field class="formfield" appearance="outline">
<cnsl-label>{{ 'IDP.USERNAMEMAPPING' | translate }}</cnsl-label>
<mat-select formControlName="usernameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINGFIELD.'+field | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
</div>
</ng-container>
<div class="btn-wrapper">
<button color="primary" mat-raised-button class="continue-button"
[disabled]="oidcConfigForm.invalid || (canWrite | async) === false" type="submit">
{{ 'ACTIONS.SAVE' | translate }}
</button>
<input cnslInput [matChipInputFor]="chipScopesList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" [matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addScope($event)">
</cnsl-form-field>
<button (click)="addScope($event)" mat-icon-button>
<mat-icon>add</mat-icon>
</button>
</div>
</form>
<cnsl-form-field appearance="outline" class="formfield fullwidth">
<mat-chip-list class="chip-list" #chipScopesList aria-label="scope selection">
<mat-chip class="chip" *ngFor="let scope of scopesList?.value" selectable="false"
removable (removed)="removeScope(scope)" [disabled]="(canWrite | async) === false">
{{scope}} <mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
</mat-chip-list>
</cnsl-form-field>
<cnsl-form-field class="formfield" appearance="outline">
<cnsl-label>{{ 'IDP.IDPDISPLAYNAMMAPPING' | translate }}</cnsl-label>
<mat-select formControlName="displayNameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINGFIELD.'+field | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-form-field class="formfield" appearance="outline">
<cnsl-label>{{ 'IDP.USERNAMEMAPPING' | translate }}</cnsl-label>
<mat-select formControlName="usernameMapping">
<mat-option *ngFor="let field of mappingFields" [value]="field">
{{ 'IDP.MAPPINGFIELD.'+field | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
</div>
</ng-container>
<div class="btn-wrapper">
<button color="primary" mat-raised-button class="continue-button"
[disabled]="oidcConfigForm.invalid || (canWrite | async) === false" type="submit">
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</form>
</ng-container>
<ng-container *ngIf="idp?.jwtConfig && jwtConfigForm">
<h2>{{'IDP.JWT.TITLE' | translate}}</h2>
<p>{{'IDP.JWT.DESCRIPTION' | translate}}</p>
<form (ngSubmit)="updateJwtConfig()">
<ng-container [formGroup]="jwtConfigForm">
<div class="idp-content">
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.ISSUER' | translate }}</cnsl-label>
<input cnslInput formControlName="issuer" />
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.JWT.HEADERNAME' | translate }}</cnsl-label>
<input cnslInput formControlName="headerName" />
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.JWT.JWTENDPOINT' | translate }}</cnsl-label>
<input cnslInput formControlName="jwtEndpoint" />
</cnsl-form-field>
<cnsl-form-field appearance="outline" class="formfield">
<cnsl-label>{{ 'IDP.JWT.JWTKEYSENDPOINT' | translate }}</cnsl-label>
<input cnslInput formControlName="keysEndpoint" />
</cnsl-form-field>
</div>
</ng-container>
<div class="btn-wrapper">
<button color="primary" mat-raised-button class="continue-button"
[disabled]="jwtConfigForm.invalid || (canWrite | async) === false" type="submit">
{{ 'ACTIONS.SAVE' | translate }}
</button>
</div>
</form>
</ng-container>
</div>
</app-detail-layout>
@@ -5,15 +5,20 @@
@media only screen and (max-width: 450px) {
padding: 4rem 1rem 2rem 1rem;
}
.idp-form {
border-top: 1px solid #ffffff20;
padding-top: 1rem;
}
}
.content {
.idp-content {
display: flex;
flex-direction: row;
margin: 0 -.5rem;
flex-wrap: wrap;
.desc {
.idp-desc {
flex-basis: 100%;
margin: 0 .5rem;
margin-bottom: 1rem;
+164 -25
View File
@@ -3,18 +3,28 @@ import { Location } from '@angular/common';
import { Component, Injector, OnDestroy, Type } from '@angular/core';
import { AbstractControl, FormControl, FormGroup, Validators } from '@angular/forms';
import { MatChipInputEvent } from '@angular/material/chips';
import { ActivatedRoute } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { Observable, Subject } from 'rxjs';
import { switchMap, take, takeUntil } from 'rxjs/operators';
import { UpdateIDPOIDCConfigRequest, UpdateIDPRequest } from 'src/app/proto/generated/zitadel/admin_pb';
import { IDPStylingType, OIDCMappingField } from 'src/app/proto/generated/zitadel/idp_pb';
import { UpdateOrgIDPOIDCConfigRequest, UpdateOrgIDPRequest } from 'src/app/proto/generated/zitadel/management_pb';
import {
UpdateIDPJWTConfigRequest,
UpdateIDPOIDCConfigRequest,
UpdateIDPRequest,
} from 'src/app/proto/generated/zitadel/admin_pb';
import { IDP, IDPState, IDPStylingType, OIDCMappingField } from 'src/app/proto/generated/zitadel/idp_pb';
import {
UpdateOrgIDPJWTConfigRequest,
UpdateOrgIDPOIDCConfigRequest,
UpdateOrgIDPRequest,
} from 'src/app/proto/generated/zitadel/management_pb';
import { AdminService } from 'src/app/services/admin.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.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';
import { WarnDialogComponent } from '../warn-dialog/warn-dialog.component';
@Component({
selector: 'app-idp',
@@ -28,13 +38,18 @@ export class IdpComponent implements OnDestroy {
public showIdSecretSection: boolean = false;
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
private service!: ManagementService | AdminService;
public PolicyComponentServiceType: any = PolicyComponentServiceType;
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];
public idp!: IDP.AsObject;
private destroy$: Subject<void> = new Subject();
public projectId: string = '';
public idpForm!: FormGroup;
public oidcConfigForm!: FormGroup;
public jwtConfigForm!: FormGroup;
IDPState: any = IDPState;
public canWrite: Observable<boolean> = this.authService.isAllowed([this.serviceType === PolicyComponentServiceType.ADMIN ?
'iam.idp.write' : this.serviceType === PolicyComponentServiceType.MGMT ?
@@ -44,8 +59,10 @@ export class IdpComponent implements OnDestroy {
private toast: ToastService,
private injector: Injector,
private route: ActivatedRoute,
private router: Router,
private _location: Location,
private authService: GrpcAuthService,
private dialog: MatDialog,
) {
this.idpForm = new FormGroup({
id: new FormControl({ disabled: true, value: '' }, [Validators.required]),
@@ -63,6 +80,13 @@ export class IdpComponent implements OnDestroy {
usernameMapping: new FormControl(0),
});
this.jwtConfigForm = new FormGroup({
jwtEndpoint: new FormControl('', [Validators.required]),
issuer: new FormControl('', [Validators.required]),
keysEndpoint: new FormControl('', [Validators.required]),
headerName: new FormControl('', [Validators.required]),
});
this.route.data.pipe(
takeUntil(this.destroy$),
switchMap(data => {
@@ -95,20 +119,26 @@ export class IdpComponent implements OnDestroy {
(this.service as ManagementService).getOrgIDPByID(id).then(resp => {
if (resp.idp) {
const idpObject = resp.idp;
this.idpForm.patchValue(idpObject);
if (idpObject.oidcConfig) {
this.oidcConfigForm.patchValue(idpObject.oidcConfig);
this.idp = resp.idp;
this.idpForm.patchValue(this.idp);
if (this.idp.oidcConfig) {
this.oidcConfigForm.patchValue(this.idp.oidcConfig);
} else if (this.idp.jwtConfig) {
this.jwtConfigForm.patchValue(this.idp.jwtConfig);
this.jwtIssuer?.setValue(this.idp.jwtConfig.issuer);
}
}
});
} else if (this.serviceType === PolicyComponentServiceType.ADMIN) {
(this.service as AdminService).getIDPByID(id).then(resp => {
if (resp.idp) {
const idpObject = resp.idp;
this.idpForm.patchValue(idpObject);
if (idpObject.oidcConfig) {
this.oidcConfigForm.patchValue(idpObject.oidcConfig);
this.idp = resp.idp;
this.idpForm.patchValue(this.idp);
if (this.idp.oidcConfig) {
this.oidcConfigForm.patchValue(this.idp.oidcConfig);
} else if (this.idp.jwtConfig) {
this.jwtConfigForm.patchValue(this.idp.jwtConfig);
this.jwtIssuer?.setValue(this.idp.jwtConfig.issuer);
}
}
});
@@ -122,11 +152,9 @@ export class IdpComponent implements OnDestroy {
if (canWrite) {
this.idpForm.enable();
this.oidcConfigForm.enable();
this.id?.disable();
} else {
this.idpForm.disable();
this.oidcConfigForm.disable();
this.id?.disable();
}
});
}
@@ -136,32 +164,98 @@ export class IdpComponent implements OnDestroy {
this.destroy$.complete();
}
public deleteIdp(): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'IDP.DELETE_TITLE',
descriptionKey: 'IDP.DELETE_DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
(this.service as ManagementService).removeOrgIDP(this.idp.id).then(() => {
this.toast.showInfo('IDP.TOAST.DELETED', true);
this.router.navigate(this.backroutes);
}).catch((error: any) => {
this.toast.showError(error);
});
} else if (this.serviceType === PolicyComponentServiceType.ADMIN) {
(this.service as AdminService).removeIDP(this.idp.id).then(() => {
this.toast.showInfo('IDP.TOAST.DELETED', true);
this.router.navigate(this.backroutes);
}).catch((error: any) => {
this.toast.showError(error);
});
}
}
});
}
public changeState(state: IDPState): void {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
if (state === IDPState.IDP_STATE_ACTIVE) {
(this.service as ManagementService).reactivateOrgIDP(this.idp.id).then(() => {
this.idp.state = state;
this.toast.showInfo('IDP.TOAST.REACTIVATED', true);
}).catch((error: any) => {
this.toast.showError(error);
});
} else if (state === IDPState.IDP_STATE_INACTIVE) {
(this.service as ManagementService).deactivateOrgIDP(this.idp.id).then(() => {
this.idp.state = state;
this.toast.showInfo('IDP.TOAST.DEACTIVATED', true);
}).catch((error: any) => {
this.toast.showError(error);
});
}
} else if (this.serviceType === PolicyComponentServiceType.ADMIN) {
if (state === IDPState.IDP_STATE_ACTIVE) {
(this.service as AdminService).reactivateIDP(this.idp.id).then(() => {
this.idp.state = state;
this.toast.showInfo('IDP.TOAST.REACTIVATED', true);
}).catch((error: any) => {
this.toast.showError(error);
});
} else if (state === IDPState.IDP_STATE_INACTIVE) {
(this.service as AdminService).deactivateIDP(this.idp.id).then(() => {
this.idp.state = state;
this.toast.showInfo('IDP.TOAST.DEACTIVATED', true);
}).catch((error: any) => {
this.toast.showError(error);
});
}
}
}
public updateIdp(): void {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
const req = new UpdateOrgIDPRequest();
req.setIdpId(this.id?.value);
req.setIdpId(this.idp.id);
req.setName(this.name?.value);
req.setStylingType(this.stylingType?.value);
req.setAutoRegister(this.autoRegister?.value);
(this.service as ManagementService).updateOrgIDP(req).then(() => {
this.toast.showInfo('IDP.TOAST.SAVED', true);
// this.router.navigate(['idp', ]);
}).catch(error => {
this.toast.showError(error);
});
} else if (this.serviceType === PolicyComponentServiceType.ADMIN) {
const req = new UpdateIDPRequest();
req.setIdpId(this.id?.value);
req.setIdpId(this.idp.id);
req.setName(this.name?.value);
req.setStylingType(this.stylingType?.value);
req.setAutoRegister(this.autoRegister?.value);
(this.service as AdminService).updateIDP(req).then(() => {
this.toast.showInfo('IDP.TOAST.SAVED', true);
// this.router.navigate(['idp', ]);
}).catch(error => {
this.toast.showError(error);
});
@@ -172,7 +266,7 @@ export class IdpComponent implements OnDestroy {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
const req = new UpdateOrgIDPOIDCConfigRequest();
req.setIdpId(this.id?.value);
req.setIdpId(this.idp.id);
req.setClientId(this.clientId?.value);
req.setClientSecret(this.clientSecret?.value);
req.setIssuer(this.issuer?.value);
@@ -182,14 +276,13 @@ export class IdpComponent implements OnDestroy {
(this.service as ManagementService).updateOrgIDPOIDCConfig(req).then((oidcConfig) => {
this.toast.showInfo('IDP.TOAST.SAVED', true);
// this.router.navigate(['idp', ]);
}).catch(error => {
this.toast.showError(error);
});
} else if (this.serviceType === PolicyComponentServiceType.ADMIN) {
const req = new UpdateIDPOIDCConfigRequest();
req.setIdpId(this.id?.value);
req.setIdpId(this.idp.id);
req.setClientId(this.clientId?.value);
req.setClientSecret(this.clientSecret?.value);
req.setIssuer(this.issuer?.value);
@@ -199,6 +292,39 @@ export class IdpComponent implements OnDestroy {
(this.service as AdminService).updateIDPOIDCConfig(req).then((oidcConfig) => {
this.toast.showInfo('IDP.TOAST.SAVED', true);
}).catch(error => {
this.toast.showError(error);
});
}
}
public updateJwtConfig(): void {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
const req = new UpdateOrgIDPJWTConfigRequest();
req.setIdpId(this.idp.id);
req.setIssuer(this.jwtIssuer?.value);
req.setHeaderName(this.headerName?.value);
req.setJwtEndpoint(this.jwtEndpoint?.value);
req.setKeysEndpoint(this.keyEndpoint?.value);
(this.service as ManagementService).updateOrgIDPJWTConfig(req).then((jwtConfig) => {
this.toast.showInfo('IDP.TOAST.SAVED', true);
// this.router.navigate(['idp', ]);
}).catch(error => {
this.toast.showError(error);
});
} else if (this.serviceType === PolicyComponentServiceType.ADMIN) {
const req = new UpdateIDPJWTConfigRequest();
req.setIdpId(this.idp.id);
req.setIssuer(this.jwtIssuer?.value);
req.setHeaderName(this.headerName?.value);
req.setJwtEndpoint(this.jwtEndpoint?.value);
req.setKeysEndpoint(this.keyEndpoint?.value);
(this.service as AdminService).updateIDPJWTConfig(req).then((jwtConfig) => {
this.toast.showInfo('IDP.TOAST.SAVED', true);
// this.router.navigate(['idp', ]);
}).catch(error => {
this.toast.showError(error);
@@ -243,10 +369,6 @@ export class IdpComponent implements OnDestroy {
}
}
public get id(): AbstractControl | null {
return this.idpForm.get('id');
}
public get name(): AbstractControl | null {
return this.idpForm.get('name');
}
@@ -282,4 +404,21 @@ export class IdpComponent implements OnDestroy {
public get usernameMapping(): AbstractControl | null {
return this.oidcConfigForm.get('usernameMapping');
}
public get jwtIssuer(): AbstractControl | null {
return this.jwtConfigForm.get('issuer');
}
public get jwtEndpoint(): AbstractControl | null {
return this.jwtConfigForm.get('jwtEndpoint');
}
public get keyEndpoint(): AbstractControl | null {
return this.jwtConfigForm.get('keysEndpoint');
}
public get headerName(): AbstractControl | null {
return this.jwtConfigForm.get('headerName');
}
}
@@ -5,13 +5,16 @@ import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatChipsModule } from '@angular/material/chips';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatSelectModule } from '@angular/material/select';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module';
import { InputModule } from 'src/app/modules/input/input.module';
import { InfoRowModule } from '../info-row/info-row.module';
import { InfoSectionModule } from '../info-section/info-section.module';
import { WarnDialogModule } from '../warn-dialog/warn-dialog.module';
import { IdpRoutingModule } from './idp-routing.module';
import { IdpComponent } from './idp.component';
@@ -24,12 +27,15 @@ import { IdpComponent } from './idp.component';
ReactiveFormsModule,
InputModule,
MatButtonModule,
WarnDialogModule,
MatIconModule,
InfoSectionModule,
MatMenuModule,
MatTooltipModule,
MatSelectModule,
TranslateModule,
MatCheckboxModule,
InfoRowModule,
MatChipsModule,
DetailLayoutModule,
],
@@ -79,4 +79,34 @@
</div>
</div>
</div>
</div>
</div>
<div class="info-row" *ngIf="idp">
<div class="info width">
<p class="title">{{ 'IDP.ID' | translate }}</p>
<div class="copy-row">
<button [disabled]="copied == idp.id"
[matTooltip]="(copied != idp.id ? 'ACTIONS.COPY' : 'ACTIONS.COPIED' ) | translate"
appCopyToClipboard [valueToCopy]="idp.id" (copiedValue)="copied = $event">
{{idp.id}}
</button>
</div>
</div>
<div class="info">
<p class="title">{{ 'IDP.DETAIL.DATECREATED' | translate }}</p>
<p class="desc">{{idp?.details?.creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}</p>
</div>
<div class="info">
<p class="title">{{ 'IDP.DETAIL.DATECHANGED' | translate }}</p>
<p class="desc">{{idp?.details?.changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}</p>
</div>
<div class="info">
<p class="title">{{ 'IDP.STATE' | translate }}</p>
<p *ngIf="idp && idp.state !== undefined" class="state"
[ngClass]="{'active': idp.state === IDPState.IDP_STATE_ACTIVE, 'inactive': idp.state === IDPState.IDP_STATE_INACTIVE}">{{'IDP.STATES.'+idp.state
| translate}}</p>
</div>
</div>
@@ -1,6 +1,7 @@
import { HttpClient } from '@angular/common/http';
import { Component, Input, OnInit } from '@angular/core';
import { App, AppState } from 'src/app/proto/generated/zitadel/app_pb';
import { IDP, IDPState } from 'src/app/proto/generated/zitadel/idp_pb';
import { User, UserState } from 'src/app/proto/generated/zitadel/user_pb';
@Component({
@@ -11,8 +12,11 @@ import { User, UserState } from 'src/app/proto/generated/zitadel/user_pb';
export class InfoRowComponent implements OnInit {
@Input() public user!: User.AsObject;
@Input() public app!: App.AsObject;
@Input() public idp!: IDP.AsObject;
public UserState: any = UserState;
public AppState: any = AppState;
public IDPState: any = IDPState;
public copied: string = '';
public environmentMap: { [key: string]: string; } = {};
@@ -1,16 +0,0 @@
<div class="sp_wrapper">
<mat-spinner diameter="30" *ngIf="loading$ | async"></mat-spinner>
</div>
<div class="mfa-list">
<div class="mfa" *ngFor="let mfa of mfas">
<button [disabled]="disabled" mat-icon-button (click)="removeMfa(mfa)" class="rm">
<mat-icon matTooltip="{{'ACTIONS.REMOVE' | translate}}">
remove_circle</mat-icon>
</button>
{{(componentType == LoginMethodComponentType.SecondFactor ? 'MFA.SECONDFACTORTYPES.':
LoginMethodComponentType.MultiFactor ? 'MFA.MULTIFACTORTYPES.': '')+mfa | translate}}
</div>
<div class="new-mfa" [ngClass]="{'disabled': disabled}" (click)="!disabled ? addMfa(): null" matRipple>
<mat-icon>add</mat-icon>
</div>
</div>
@@ -1,62 +0,0 @@
.t .sp_wrapper {
display: block;
}
.mfa-list {
display: flex;
flex-wrap: wrap;
margin: 0 -.5rem;
.mfa {
background-color: #6a506e;
color: white;
}
.mfa,
.new-mfa {
border: 1px solid var(--grey);
border-radius: .5rem;
display: grid;
align-items: center;
justify-content: center;
margin: .5rem;
padding: 10px;
position: relative;
min-height: 70px;
min-width: 150px;
.rm {
position: absolute;
display: none;
top: -2px;
left: -2px;
transform: translateX(-50%) translateY(-50%);
cursor: pointer;
color: var(--warn);
transition: all .2s ease;
&[disabled] {
display: none;
}
}
&:not(.disabled) {
cursor: default;
&:hover {
.rm {
display: block;
}
}
}
&.disabled {
opacity: .5;
cursor: not-allowed;
}
}
.new-mfa:not(.disabled) {
cursor: pointer;
}
}
@@ -1,224 +0,0 @@
import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatPaginator } from '@angular/material/paginator';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, Observable } from 'rxjs';
import {
AddMultiFactorToLoginPolicyRequest as AdminAddMultiFactorToLoginPolicyRequest,
AddSecondFactorToLoginPolicyRequest as AdminAddSecondFactorToLoginPolicyRequest,
RemoveMultiFactorFromLoginPolicyRequest as AdminRemoveMultiFactorFromLoginPolicyRequest,
RemoveSecondFactorFromLoginPolicyRequest as AdminRemoveSecondFactorFromLoginPolicyRequest,
} from 'src/app/proto/generated/zitadel/admin_pb';
import {
AddMultiFactorToLoginPolicyRequest as MgmtAddMultiFactorToLoginPolicyRequest,
AddSecondFactorToLoginPolicyRequest as MgmtAddSecondFactorToLoginPolicyRequest,
RemoveMultiFactorFromLoginPolicyRequest as MgmtRemoveMultiFactorFromLoginPolicyRequest,
RemoveSecondFactorFromLoginPolicyRequest as MgmtRemoveSecondFactorFromLoginPolicyRequest,
} from 'src/app/proto/generated/zitadel/management_pb';
import { MultiFactorType, SecondFactorType } from 'src/app/proto/generated/zitadel/policy_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';
import { WarnDialogComponent } from '../warn-dialog/warn-dialog.component';
import { DialogAddTypeComponent } from './dialog-add-type/dialog-add-type.component';
export enum LoginMethodComponentType {
MultiFactor = 1,
SecondFactor = 2,
}
@Component({
selector: 'app-mfa-table',
templateUrl: './mfa-table.component.html',
styleUrls: ['./mfa-table.component.scss'],
})
export class MfaTableComponent implements OnInit {
public LoginMethodComponentType: any = LoginMethodComponentType;
@Input() componentType!: LoginMethodComponentType;
@Input() public serviceType!: PolicyComponentServiceType;
@Input() service!: AdminService | ManagementService;
@Input() disabled: boolean = false;
@ViewChild(MatPaginator) public paginator!: MatPaginator;
public mfas: Array<MultiFactorType | SecondFactorType> = [];
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public PolicyComponentServiceType: any = PolicyComponentServiceType;
constructor(public translate: TranslateService, private toast: ToastService, private dialog: MatDialog) { }
public ngOnInit(): void {
this.getData();
}
public removeMfa(type: MultiFactorType | SecondFactorType): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'MFA.DELETE.TITLE',
descriptionKey: 'MFA.DELETE.DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
if (this.componentType === LoginMethodComponentType.MultiFactor) {
const req = new MgmtRemoveMultiFactorFromLoginPolicyRequest();
req.setType(type as MultiFactorType);
(this.service as ManagementService).removeMultiFactorFromLoginPolicy(req).then(() => {
this.toast.showInfo('MFA.TOAST.DELETED', true);
this.refreshPageAfterTimout(2000);
});
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
const req = new MgmtRemoveSecondFactorFromLoginPolicyRequest();
req.setType(type as SecondFactorType);
(this.service as ManagementService).removeSecondFactorFromLoginPolicy(req).then(() => {
this.toast.showInfo('MFA.TOAST.DELETED', true);
this.refreshPageAfterTimout(2000);
});
}
} else if (this.serviceType === PolicyComponentServiceType.ADMIN) {
if (this.componentType === LoginMethodComponentType.MultiFactor) {
const req = new AdminRemoveMultiFactorFromLoginPolicyRequest();
req.setType(type as MultiFactorType);
(this.service as AdminService).removeMultiFactorFromLoginPolicy(req).then(() => {
this.toast.showInfo('MFA.TOAST.DELETED', true);
this.refreshPageAfterTimout(2000);
});
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
const req = new AdminRemoveSecondFactorFromLoginPolicyRequest();
req.setType(type as SecondFactorType);
(this.service as AdminService).removeSecondFactorFromLoginPolicy(req).then(() => {
this.toast.showInfo('MFA.TOAST.DELETED', true);
this.refreshPageAfterTimout(2000);
});
}
}
}
});
}
public addMfa(): void {
let selection: any[] = [];
if (this.componentType === LoginMethodComponentType.MultiFactor) {
selection = [MultiFactorType.MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION];
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
selection = [SecondFactorType.SECOND_FACTOR_TYPE_U2F, SecondFactorType.SECOND_FACTOR_TYPE_OTP];
}
this.mfas.forEach(mfa => {
const index = selection.findIndex(sel => sel === mfa);
if (index > -1) {
selection.splice(index, 1);
}
});
const dialogRef = this.dialog.open(DialogAddTypeComponent, {
data: {
title: 'MFA.CREATE.TITLE',
desc: 'MFA.CREATE.DESCRIPTION',
componentType: this.componentType,
types: selection,
},
width: '400px',
});
dialogRef.afterClosed().subscribe((mfaType: MultiFactorType | SecondFactorType) => {
if (mfaType) {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
if (this.componentType === LoginMethodComponentType.MultiFactor) {
const req = new MgmtAddMultiFactorToLoginPolicyRequest();
req.setType(mfaType as MultiFactorType);
(this.service as ManagementService).addMultiFactorToLoginPolicy(req).then(() => {
this.refreshPageAfterTimout(2000);
}).catch(error => {
this.toast.showError(error);
});
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
const req = new MgmtAddSecondFactorToLoginPolicyRequest();
req.setType(mfaType as SecondFactorType);
(this.service as ManagementService).addSecondFactorToLoginPolicy(req).then(() => {
this.refreshPageAfterTimout(2000);
}).catch(error => {
this.toast.showError(error);
});
}
} else if (this.serviceType === PolicyComponentServiceType.ADMIN) {
if (this.componentType === LoginMethodComponentType.MultiFactor) {
const req = new AdminAddMultiFactorToLoginPolicyRequest();
req.setType(mfaType as MultiFactorType);
(this.service as AdminService).addMultiFactorToLoginPolicy(req).then(() => {
this.refreshPageAfterTimout(2000);
}).catch(error => {
this.toast.showError(error);
});
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
const req = new AdminAddSecondFactorToLoginPolicyRequest();
req.setType(mfaType as SecondFactorType);
(this.service as AdminService).addSecondFactorToLoginPolicy(req).then(() => {
this.refreshPageAfterTimout(2000);
}).catch(error => {
this.toast.showError(error);
});
}
}
}
});
}
private async getData(): Promise<void> {
this.loadingSubject.next(true);
if (this.serviceType === PolicyComponentServiceType.MGMT) {
if (this.componentType === LoginMethodComponentType.MultiFactor) {
(this.service as ManagementService).listLoginPolicyMultiFactors().then(resp => {
this.mfas = resp.resultList;
this.loadingSubject.next(false);
}).catch(error => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
(this.service as ManagementService).listLoginPolicySecondFactors().then(resp => {
this.mfas = resp.resultList;
this.loadingSubject.next(false);
}).catch(error => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
}
} else if (this.serviceType === PolicyComponentServiceType.ADMIN) {
if (this.componentType === LoginMethodComponentType.MultiFactor) {
(this.service as AdminService).listLoginPolicyMultiFactors().then(resp => {
this.mfas = resp.resultList;
this.loadingSubject.next(false);
}).catch(error => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
(this.service as AdminService).listLoginPolicySecondFactors().then(resp => {
this.mfas = resp.resultList;
this.loadingSubject.next(false);
}).catch(error => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
}
}
}
public refreshPageAfterTimout(to: number): void {
setTimeout(() => {
this.getData();
}, to);
}
}
@@ -1,46 +0,0 @@
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 { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
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/localized-date-pipe.module';
import { TimestampToDatePipeModule } from 'src/app/pipes/timestamp-to-date-pipe/timestamp-to-date-pipe.module';
import { TruncatePipeModule } from 'src/app/pipes/truncate-pipe/truncate-pipe.module';
import { MfaTableComponent } from './mfa-table.component';
import { DialogAddTypeComponent } from './dialog-add-type/dialog-add-type.component';
import { InputModule } from '../input/input.module';
import { MatSelectModule } from '@angular/material/select';
import { MatRippleModule } from '@angular/material/core';
@NgModule({
declarations: [MfaTableComponent, DialogAddTypeComponent],
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
MatButtonModule,
MatIconModule,
InputModule,
MatSelectModule,
MatTooltipModule,
TranslateModule,
TimestampToDatePipeModule,
HasRoleModule,
MatProgressSpinnerModule,
MatRippleModule,
],
exports: [
MfaTableComponent,
],
})
export class MfaTableModule { }
@@ -1,24 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { MatSelectModule } from '@angular/material/select';
import { TranslateModule } from '@ngx-translate/core';
import { InputModule } from 'src/app/modules/input/input.module';
import { AddIdpDialogComponent } from './add-idp-dialog.component';
@NgModule({
declarations: [AddIdpDialogComponent],
imports: [
CommonModule,
MatDialogModule,
MatButtonModule,
TranslateModule,
InputModule,
MatSelectModule,
FormsModule,
],
})
export class AddIdpDialogModule { }
@@ -13,8 +13,6 @@
</mat-select>
</cnsl-form-field>
<p>{{'LOGINPOLICY.ADDIDP.SELECTIDPS' | translate}}</p>
<cnsl-form-field class="full-width" appearance="outline">
<cnsl-label>{{ 'LOGINPOLICY.ADDIDP.SELECTIDPS' | translate }}</cnsl-label>
<mat-select [(ngModel)]="idp">
@@ -5,7 +5,7 @@ import { IDPQuery } from 'src/app/proto/generated/zitadel/management_pb';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentServiceType } from '../../policy-component-types.enum';
import { PolicyComponentServiceType } from '../../../policy-component-types.enum';
@Component({
selector: 'app-add-idp-dialog',
@@ -0,0 +1,21 @@
<div class="idps">
<div class="idp mat-elevation-z1"
*ngFor="let idp of idps">
<img src="../../../assets/images/google.png"
*ngIf="idp.idpName.toLowerCase() == 'google'" alt="google" />
<div>
<span class="name">{{idp.idpName}}</span>
<span class="meta-info">{{ 'IDP.TYPES.'+idp.idpType | translate }}</span>
<!-- <span class="meta-info">{{ 'IDP.ID' | translate }}: {{idp.idpId}}</span> -->
</div>
<span class="fill-space"></span>
<button color="warn" *ngIf="!disabled" mat-icon-button (click)="removeIdp(idp)" class="rm">
<mat-icon matTooltip="{{'ACTIONS.REMOVE' | translate}}">remove_circle</mat-icon>
</button>
</div>
<button mat-raised-button color="primary" [disabled]="disabled"
class="new-idp" (click)="openDialog()" >
<span>{{'IDP.ADD' | translate}}</span>
<mat-icon class="icon">add</mat-icon>
</button>
</div>
@@ -0,0 +1,63 @@
.idps {
display: flex;
flex-direction: column;
.idp {
display: flex;
align-items: center;
padding: 10px;
border-radius: .5rem;
position: relative;
margin-bottom: .5rem;
min-height: 40px;
img {
height: 30px;
width: 30px;
margin: .5rem;
border-radius: .5rem;
object-fit: contain;
}
div {
display: block;
margin-left: .5rem;
* {
display: block;
}
}
.meta-info {
font-size: 14px;
color: var(--grey);
}
.fill-space {
flex: 1;
}
.rm {
display: none;
transition: all .2s ease;
cursor: pointer;
}
&:hover {
.rm {
display: block;
}
}
}
.new-idp {
display: flex;
align-items: center;
align-self: flex-end;
margin-top: .5rem;
.icon {
margin-left: .5rem;
}
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginPolicyIdpsComponent } from './login-policy-idps.component';
describe('LoginPolicyIdpsComponent', () => {
let component: LoginPolicyIdpsComponent;
let fixture: ComponentFixture<LoginPolicyIdpsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [LoginPolicyIdpsComponent],
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(LoginPolicyIdpsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,108 @@
import { Component, Input, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { IDP, IDPLoginPolicyLink, IDPOwnerType, IDPStylingType } from 'src/app/proto/generated/zitadel/idp_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 '../../policy-component-types.enum';
import { AddIdpDialogComponent } from './add-idp-dialog/add-idp-dialog.component';
@Component({
selector: 'cnsl-login-policy-idps',
templateUrl: './login-policy-idps.component.html',
styleUrls: ['./login-policy-idps.component.scss'],
})
export class LoginPolicyIdpsComponent implements OnInit {
@Input() public disabled: boolean = true;
@Input() public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
@Input() public service!: ManagementService | AdminService;
public loading: boolean = false;
public idps: IDPLoginPolicyLink.AsObject[] = [];
public IDPStylingType: any = IDPStylingType;
constructor(
private toast: ToastService,
private dialog: MatDialog,
) { }
ngOnInit(): void {
this.getIdps().then(resp => {
this.idps = resp;
console.log(this.idps);
});
}
private async getIdps(): Promise<IDPLoginPolicyLink.AsObject[]> {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
return (this.service as ManagementService).listLoginPolicyIDPs()
.then((resp) => {
return resp.resultList;
});
case PolicyComponentServiceType.ADMIN:
return (this.service as AdminService).listLoginPolicyIDPs()
.then((providers) => {
return providers.resultList;
});
}
}
private addIdp(idp: IDP.AsObject | IDP.AsObject, ownerType: IDPOwnerType): Promise<any> {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
return (this.service as ManagementService).addIDPToLoginPolicy(idp.id, ownerType);
case PolicyComponentServiceType.ADMIN:
return (this.service as AdminService).addIDPToLoginPolicy(idp.id);
}
}
public openDialog(): void {
const dialogRef = this.dialog.open(AddIdpDialogComponent, {
data: {
serviceType: this.serviceType,
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp && resp.idp && resp.type) {
this.addIdp(resp.idp, resp.type).then(() => {
this.loading = true;
setTimeout(() => {
this.getIdps();
}, 1000);
}).catch(error => {
this.toast.showError(error);
});
}
});
}
public removeIdp(idp: IDPLoginPolicyLink.AsObject): void {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
(this.service as ManagementService).removeIDPFromLoginPolicy(idp.idpId).then(() => {
const index = this.idps.findIndex(temp => temp === idp);
if (index > -1) {
this.idps.splice(index, 1);
}
}, error => {
this.toast.showError(error);
});
break;
case PolicyComponentServiceType.ADMIN:
(this.service as AdminService).removeIDPFromLoginPolicy(idp.idpId).then(() => {
const index = this.idps.findIndex(temp => temp === idp);
if (index > -1) {
this.idps.splice(index, 1);
}
}, error => {
this.toast.showError(error);
});
break;
}
}
}
@@ -6,131 +6,35 @@
<div class="spinner-wr">
<mat-spinner diameter="30" *ngIf="loading" color="primary"></mat-spinner>
</div>
<ng-container *ngIf="serviceType === PolicyComponentServiceType.MGMT">
<ng-template appHasRole [appHasRole]="['policy.delete']">
<button *ngIf="!isDefault" color="primary" matTooltip="{{'POLICY.RESET' | translate}}" color="warn" (click)="removePolicy()"
mat-stroked-button>
{{'POLICY.RESET' | translate}}
</button>
</ng-template>
<ng-template appHasRole [appHasRole]="['policy.delete']">
<button *ngIf="!isDefault" [disabled]="serviceType == PolicyComponentServiceType.MGMT && (['login_policy'] | hasFeature | async) == false" color="primary" matTooltip="{{'POLICY.RESET' | translate}}" color="warn" (click)="removePolicy()"
mat-stroked-button>
{{'POLICY.RESET' | translate}}
</button>
</ng-template>
<ng-template appHasRole [appHasRole]="['policy.write']">
<button *ngIf="isDefault" color="primary" matTooltip="{{'POLICY.CREATECUSTOM' | translate}}" (click)="savePolicy()"
mat-raised-button>
{{'POLICY.CREATECUSTOM' | translate}}
</button>
</ng-template>
</ng-container>
<ng-template appHasRole [appHasRole]="['policy.write']">
<button *ngIf="isDefault" [disabled]="serviceType == PolicyComponentServiceType.MGMT && (['login_policy'] | hasFeature | async) == false" color="primary" matTooltip="{{'POLICY.CREATECUSTOM' | translate}}" (click)="savePolicy()"
mat-raised-button>
{{'POLICY.CREATECUSTOM' | translate}}
</button>
</ng-template>
</ng-container>
<div class="content" *ngIf="loginData">
<div class="row">
<mat-slide-toggle class="toggle" color="primary" [disabled]="disabled || serviceType == PolicyComponentServiceType.MGMT && (['login_policy.username_login'] | hasFeature | async) == false" ngDefaultControl
[(ngModel)]="loginData.allowUsernamePassword">
{{'POLICY.DATA.ALLOWUSERNAMEPASSWORD' | translate}}
</mat-slide-toggle>
<cnsl-info-section *ngIf="serviceType == PolicyComponentServiceType.MGMT && (['login_policy.username_login'] | hasFeature | async) == false; else usernameInfo" [featureLink]="['/org/features']" class="info" type="WARN">
<span [innerHTML]="'FEATURES.NOTAVAILABLE' | translate: ({value: 'login_policy.username_login'})"></span>
</cnsl-info-section>
<app-card title="{{ 'IDP.LIST.ACTIVETITLE' | translate }}"
[expanded]="true">
<ng-template #usernameInfo>
<cnsl-info-section class="info">
{{'POLICY.DATA.ALLOWUSERNAMEPASSWORD_DESC' | translate}}
</cnsl-info-section>
</ng-template>
</div>
<div class="row">
<mat-slide-toggle class="toggle" color="primary"
[disabled]="disabled || (serviceType == PolicyComponentServiceType.MGMT && (['login_policy.registration'] | hasFeature | async) == false)"
ngDefaultControl [(ngModel)]="loginData.allowRegister">
{{'POLICY.DATA.ALLOWREGISTER' | translate}}
</mat-slide-toggle>
<cnsl-info-section *ngIf="serviceType == PolicyComponentServiceType.MGMT && (['login_policy.registration'] | hasFeature | async) == false; else regInfo" [featureLink]="['/org/features']" class="info" type="WARN">
<span [innerHTML]="'FEATURES.NOTAVAILABLE' | translate: ({value: 'login_policy.registration'})"></span>
</cnsl-info-section>
<ng-template #regInfo>
<cnsl-info-section class="info">
{{'POLICY.DATA.ALLOWREGISTER_DESC' | translate}}
</cnsl-info-section>
</ng-template>
</div>
<div class="row">
<mat-slide-toggle class="toggle" color="primary" [disabled]="disabled || serviceType == PolicyComponentServiceType.MGMT && (['login_policy.idp'] | hasFeature | async) == false" ngDefaultControl
[(ngModel)]="loginData.allowExternalIdp">
{{'POLICY.DATA.ALLOWEXTERNALIDP' | translate}}
</mat-slide-toggle>
<cnsl-info-section *ngIf="serviceType == PolicyComponentServiceType.MGMT && (['login_policy.idp'] | hasFeature | async) == false; else idpInfo" [featureLink]="['/org/features']" class="info" type="WARN">
<span [innerHTML]="'FEATURES.NOTAVAILABLE' | translate: ({value: 'login_policy.idp'})"></span>
</cnsl-info-section>
<ng-template #idpInfo>
<cnsl-info-section class="info">
{{'POLICY.DATA.ALLOWEXTERNALIDP_DESC' | translate}}
</cnsl-info-section>
</ng-template>
</div>
<div class="row">
<mat-slide-toggle class="toggle" color="primary" [disabled]="disabled || serviceType == PolicyComponentServiceType.MGMT && (['login_policy.factors'] | hasFeature | async) == false" ngDefaultControl
[(ngModel)]="loginData.forceMfa">
{{'POLICY.DATA.FORCEMFA' | translate}}
</mat-slide-toggle>
<cnsl-info-section *ngIf="serviceType == PolicyComponentServiceType.MGMT && (['login_policy.factors'] | hasFeature | async) == false; else factorsInfo" [featureLink]="['/org/features']" class="info" type="WARN">
<span [innerHTML]="'FEATURES.NOTAVAILABLE' | translate: ({value: 'login_policy.factors'})"></span>
</cnsl-info-section>
<ng-template #factorsInfo>
<cnsl-info-section class="info">
{{'POLICY.DATA.FORCEMFA_DESC' | translate}}
</cnsl-info-section>
</ng-template>
</div>
<div class="row">
<mat-slide-toggle class="toggle" color="primary" [disabled]="disabled || serviceType == PolicyComponentServiceType.MGMT && (['login_policy.password_reset'] | hasFeature | async) == false" ngDefaultControl
[(ngModel)]="loginData.hidePasswordReset">
{{'POLICY.DATA.HIDEPASSWORDRESET' | translate}}
</mat-slide-toggle>
<cnsl-info-section *ngIf="serviceType == PolicyComponentServiceType.MGMT && (['login_policy.password_reset'] | hasFeature | async) == false; else passwordResetInfo" [featureLink]="['/org/features']" class="info" type="WARN">
<span [innerHTML]="'FEATURES.NOTAVAILABLE' | translate: ({value: 'login_policy.password_reset'})"></span>
<cnsl-info-section *ngIf="serviceType == PolicyComponentServiceType.MGMT && (['login_policy.idp'] | hasFeature | async) == false" [featureLink]="['/org/features']" class="info" type="WARN">
<span [innerHTML]="'FEATURES.NOTAVAILABLE' | translate: ({value: 'login_policy.idp'})"></span>
</cnsl-info-section>
<cnsl-login-policy-idps [serviceType]="serviceType" [service]="service" [disabled]="disabled || (serviceType == PolicyComponentServiceType.MGMT && (['login_policy.idp'] | hasFeature | async) == false)"></cnsl-login-policy-idps>
</app-card>
<ng-template #passwordResetInfo>
<cnsl-info-section class="info">
{{'POLICY.DATA.HIDEPASSWORDRESET_DESC' | translate}}
</cnsl-info-section>
</ng-template>
</div>
<div class="row">
<cnsl-form-field class="form-field" label="Access Code" required="true">
<cnsl-label>{{'LOGINPOLICY.PASSWORDLESS' | translate}}</cnsl-label>
<mat-select [(ngModel)]="loginData.passwordlessType"
[disabled]="disabled || (serviceType == PolicyComponentServiceType.MGMT && (['login_policy.passwordless'] | hasFeature | async) == false)">
<mat-option *ngFor="let pt of passwordlessTypes" [value]="pt">
{{'LOGINPOLICY.PASSWORDLESSTYPE.'+pt | translate}}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-info-section *ngIf="serviceType == PolicyComponentServiceType.MGMT && (['login_policy.passwordless'] | hasFeature | async) == false" [featureLink]="['/org/features']" class="info" type="WARN">
<span [innerHTML]="'FEATURES.NOTAVAILABLE' | translate: ({value: 'login_policy.passwordless'})"></span>
</cnsl-info-section>
</div>
</div>
<button [disabled]="disabled" class="save-button" (click)="savePolicy()" color="primary" type="submit"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
<div class="divider"></div>
<ng-container *ngIf="!isDefault">
<h3 class="subheader">{{ 'MFA.LIST.MULTIFACTORTITLE' | translate }}</h3>
<p class="subdesc">{{ 'MFA.LIST.MULTIFACTORDESCRIPTION' | translate }}</p>
<app-card title="{{ 'MFA.LIST.MULTIFACTORTITLE' | translate }}" description="{{'MFA.LIST.MULTIFACTORDESCRIPTION' | translate}}" [expanded]="false">
<cnsl-info-section *ngIf="serviceType == PolicyComponentServiceType.MGMT && (['login_policy.factors'] | hasFeature | async) == false" [featureLink]="['/org/features']" class="info" type="WARN">
<span [innerHTML]="'FEATURES.NOTAVAILABLE' | translate: ({value: 'login_policy.factors'})"></span>
</cnsl-info-section>
@@ -139,62 +43,134 @@
[componentType]="LoginMethodComponentType.MultiFactor"
[disabled]="(([serviceType == PolicyComponentServiceType.ADMIN ? 'iam.policy.write' : serviceType == PolicyComponentServiceType.MGMT ? 'policy.write' : ''] | hasRole | async) == false) || (serviceType == PolicyComponentServiceType.MGMT && (['login_policy.factors'] | hasFeature | async) == false)">
</app-mfa-table>
</app-card>
<h3 class="subheader">{{ 'MFA.LIST.SECONDFACTORTITLE' | translate }}</h3>
<p class="subdesc">{{ 'MFA.LIST.SECONDFACTORDESCRIPTION' | translate }}</p>
<app-card title="{{ 'MFA.LIST.SECONDFACTORTITLE' | translate }}" description="{{'MFA.LIST.SECONDFACTORDESCRIPTION' | translate}}" [expanded]="false">
<cnsl-info-section *ngIf="serviceType == PolicyComponentServiceType.MGMT && (['login_policy.factors'] | hasFeature | async) == false" [featureLink]="['/org/features']" class="info" type="WARN">
<span [innerHTML]="'FEATURES.NOTAVAILABLE' | translate: ({value: 'login_policy.factors'})"></span>
</cnsl-info-section>
<cnsl-info-section *ngIf="serviceType == PolicyComponentServiceType.MGMT && (['login_policy.factors'] | hasFeature | async) == false" [featureLink]="['/org/features']" class="info" type="WARN">
<span [innerHTML]="'FEATURES.NOTAVAILABLE' | translate: ({value: 'login_policy.factors'})"></span>
</cnsl-info-section>
<app-mfa-table [service]="service" [serviceType]="serviceType"
[componentType]="LoginMethodComponentType.SecondFactor"
[disabled]="([serviceType == PolicyComponentServiceType.ADMIN ? 'iam.policy.write' : serviceType == PolicyComponentServiceType.MGMT ? 'policy.write' : ''] | hasRole | async) == false || (serviceType == PolicyComponentServiceType.MGMT && (['login_policy.factors'] | hasFeature | async) == false)">
</app-mfa-table>
</app-card>
<app-mfa-table [service]="service" [serviceType]="serviceType"
[componentType]="LoginMethodComponentType.SecondFactor"
[disabled]="([serviceType == PolicyComponentServiceType.ADMIN ? 'iam.policy.write' : serviceType == PolicyComponentServiceType.MGMT ? 'policy.write' : ''] | hasRole | async) == false || (serviceType == PolicyComponentServiceType.MGMT && (['login_policy.factors'] | hasFeature | async) == false)">
</app-mfa-table>
</ng-container>
<app-card title="{{ 'POLICY.LOGIN_POLICY.ADVANCED' | translate }}" [expanded]="false">
<div class="content" *ngIf="loginData">
<div class="row">
<mat-slide-toggle class="toggle" color="primary" [disabled]="disabled || serviceType == PolicyComponentServiceType.MGMT && (['login_policy.username_login'] | hasFeature | async) == false" ngDefaultControl
[(ngModel)]="loginData.allowUsernamePassword">
{{'POLICY.DATA.ALLOWUSERNAMEPASSWORD' | translate}}
</mat-slide-toggle>
<h3 class="subheader">{{'LOGINPOLICY.IDPS' | translate}}</h3>
<cnsl-info-section *ngIf="serviceType == PolicyComponentServiceType.MGMT && (['login_policy.username_login'] | hasFeature | async) == false; else usernameInfo" [featureLink]="['/org/features']" class="info" type="WARN">
<span [innerHTML]="'FEATURES.NOTAVAILABLE' | translate: ({value: 'login_policy.username_login'})"></span>
</cnsl-info-section>
<cnsl-info-section *ngIf="serviceType == PolicyComponentServiceType.MGMT && (['login_policy.idp'] | hasFeature | async) == false" [featureLink]="['/org/features']" class="info" type="WARN">
<span [innerHTML]="'FEATURES.NOTAVAILABLE' | translate: ({value: 'login_policy.idp'})"></span>
</cnsl-info-section>
<ng-template #usernameInfo>
<cnsl-info-section class="info">
{{'POLICY.DATA.ALLOWUSERNAMEPASSWORD_DESC' | translate}}
</cnsl-info-section>
</ng-template>
</div>
<div class="row">
<mat-slide-toggle class="toggle" color="primary"
[disabled]="disabled || (serviceType == PolicyComponentServiceType.MGMT && (['login_policy.registration'] | hasFeature | async) == false)"
ngDefaultControl [(ngModel)]="loginData.allowRegister">
{{'POLICY.DATA.ALLOWREGISTER' | translate}}
</mat-slide-toggle>
<div class="idps">
<div class="idp"
[ngClass]="{'disabled': (disabled || (serviceType == PolicyComponentServiceType.MGMT && (['login_policy.idp'] | hasFeature | async) == false))}"
*ngFor="let idp of idps">
<button
[disabled]="disabled || (serviceType == PolicyComponentServiceType.MGMT && (['login_policy.idp'] | hasFeature | async) == false)"
mat-icon-button (click)="removeIdp(idp)" class="rm">
<mat-icon matTooltip="{{'ACTIONS.REMOVE' | translate}}">
remove_circle</mat-icon>
</button>
<div class="line">
<img src="../../../assets/images/google.png"
*ngIf="idp.stylingType == IDPStylingType.STYLING_TYPE_GOOGLE" alt="google" />
<div>
<span class="name">{{idp.idpName}}</span>
<span class="meta-info">{{ 'IDP.TYPE' | translate }}: {{ 'IDP.TYPES.'+idp.idpType |
translate
}}</span>
<span class="meta-info">{{ 'IDP.ID' | translate }}: {{idp.idpId}}</span>
</div>
</div>
<cnsl-info-section *ngIf="serviceType == PolicyComponentServiceType.MGMT && (['login_policy.registration'] | hasFeature | async) == false; else regInfo" [featureLink]="['/org/features']" class="info" type="WARN">
<span [innerHTML]="'FEATURES.NOTAVAILABLE' | translate: ({value: 'login_policy.registration'})"></span>
</cnsl-info-section>
<ng-template #regInfo>
<cnsl-info-section class="info">
{{'POLICY.DATA.ALLOWREGISTER_DESC' | translate}}
</cnsl-info-section>
</ng-template>
</div>
<div class="row">
<mat-slide-toggle class="toggle" color="primary" [disabled]="disabled || serviceType == PolicyComponentServiceType.MGMT && (['login_policy.idp'] | hasFeature | async) == false" ngDefaultControl
[(ngModel)]="loginData.allowExternalIdp">
{{'POLICY.DATA.ALLOWEXTERNALIDP' | translate}}
</mat-slide-toggle>
<cnsl-info-section *ngIf="serviceType == PolicyComponentServiceType.MGMT && (['login_policy.idp'] | hasFeature | async) == false; else idpInfo" [featureLink]="['/org/features']" class="info" type="WARN">
<span [innerHTML]="'FEATURES.NOTAVAILABLE' | translate: ({value: 'login_policy.idp'})"></span>
</cnsl-info-section>
<ng-template #idpInfo>
<cnsl-info-section class="info">
{{'POLICY.DATA.ALLOWEXTERNALIDP_DESC' | translate}}
</cnsl-info-section>
</ng-template>
</div>
<div class="row">
<mat-slide-toggle class="toggle" color="primary" [disabled]="disabled || serviceType == PolicyComponentServiceType.MGMT && (['login_policy.factors'] | hasFeature | async) == false" ngDefaultControl
[(ngModel)]="loginData.forceMfa">
{{'POLICY.DATA.FORCEMFA' | translate}}
</mat-slide-toggle>
<cnsl-info-section *ngIf="serviceType == PolicyComponentServiceType.MGMT && (['login_policy.factors'] | hasFeature | async) == false; else factorsInfo" [featureLink]="['/org/features']" class="info" type="WARN">
<span [innerHTML]="'FEATURES.NOTAVAILABLE' | translate: ({value: 'login_policy.factors'})"></span>
</cnsl-info-section>
<ng-template #factorsInfo>
<cnsl-info-section class="info">
{{'POLICY.DATA.FORCEMFA_DESC' | translate}}
</cnsl-info-section>
</ng-template>
</div>
<div class="row">
<mat-slide-toggle class="toggle" color="primary" [disabled]="disabled || serviceType == PolicyComponentServiceType.MGMT && (['login_policy.password_reset'] | hasFeature | async) == false" ngDefaultControl
[(ngModel)]="loginData.hidePasswordReset">
{{'POLICY.DATA.HIDEPASSWORDRESET' | translate}}
</mat-slide-toggle>
<cnsl-info-section *ngIf="serviceType == PolicyComponentServiceType.MGMT && (['login_policy.password_reset'] | hasFeature | async) == false; else passwordResetInfo" [featureLink]="['/org/features']" class="info" type="WARN">
<span [innerHTML]="'FEATURES.NOTAVAILABLE' | translate: ({value: 'login_policy.password_reset'})"></span>
</cnsl-info-section>
<ng-template #passwordResetInfo>
<cnsl-info-section class="info">
{{'POLICY.DATA.HIDEPASSWORDRESET_DESC' | translate}}
</cnsl-info-section>
</ng-template>
</div>
<div *ngIf="!disabled && ((serviceType == PolicyComponentServiceType.MGMT && (['login_policy.idp'] | hasFeature | async)) || serviceType == PolicyComponentServiceType.ADMIN)"
class="new-idp" (click)="openDialog()" matRipple>
<mat-icon>add</mat-icon>
</div>
</div>
<div class="row">
<cnsl-form-field class="form-field" label="Access Code" required="true">
<cnsl-label>{{'LOGINPOLICY.PASSWORDLESS' | translate}}</cnsl-label>
<mat-select [(ngModel)]="loginData.passwordlessType"
[disabled]="disabled || (serviceType == PolicyComponentServiceType.MGMT && (['login_policy.passwordless'] | hasFeature | async) == false)">
<mat-option *ngFor="let pt of passwordlessTypes" [value]="pt">
{{'LOGINPOLICY.PASSWORDLESSTYPE.'+pt | translate}}
</mat-option>
</mat-select>
</cnsl-form-field>
<cnsl-info-section *ngIf="serviceType == PolicyComponentServiceType.MGMT && (['login_policy.passwordless'] | hasFeature | async) == false" [featureLink]="['/org/features']" class="info" type="WARN">
<span [innerHTML]="'FEATURES.NOTAVAILABLE' | translate: ({value: 'login_policy.passwordless'})"></span>
</cnsl-info-section>
</div>
</div>
</app-card>
<button [disabled]="disabled" class="save-button" (click)="savePolicy()" color="primary" type="submit"
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
<div class="divider"></div>
<ng-template appHasRole [appHasRole]="['org.idp.read']">
<app-card title="{{ 'IDP.LIST.TITLE' | translate }}" description="{{ 'IDP.LIST.DESCRIPTION' | translate }}"
[expanded]="false">
<app-card title="{{ 'IDP.LIST.TITLE' | translate }}" description="{{ 'IDP.LIST.DESCRIPTION' | translate }}"
[expanded]="false">
<app-idp-table [service]="service" [serviceType]="serviceType"
[disabled]="([serviceType == PolicyComponentServiceType.ADMIN ? 'iam.idp.write' : serviceType == PolicyComponentServiceType.MGMT ? 'org.idp.write' : ''] | hasRole | async) == false || ((serviceType == PolicyComponentServiceType.MGMT && (['login_policy.idp'] | hasFeature | async) == false))">
</app-idp-table>
</app-card>
<app-idp-table [service]="service" [serviceType]="serviceType"
[disabled]="([serviceType == PolicyComponentServiceType.ADMIN ? 'iam.idp.write' : serviceType == PolicyComponentServiceType.MGMT ? 'org.idp.write' : ''] | hasRole | async) == false || ((serviceType == PolicyComponentServiceType.MGMT && (['login_policy.idp'] | hasFeature | async) == false))">
</app-idp-table>
</app-card>
</ng-template>
<app-policy-grid class="grid" [currentPolicy]="currentPolicy" [type]="serviceType" tagForFilter="security"></app-policy-grid>
@@ -37,95 +37,6 @@
font-size: 14px;
}
.idps {
display: flex;
margin: 0 -.5rem;
.idp {
background-color: #506e6e;
color: white;
.line {
display: flex;
align-items: center;
img {
height: 30px;
width: 30px;
margin-right: 1rem;
border-radius: .5rem;
object-fit: contain;
}
div {
flex: 1;
display: block;
* {
display: block;
}
}
}
}
.idp,
.new-idp {
display: grid;
align-items: center;
justify-content: center;
margin: .5rem;
padding: 10px;
border: 1px solid var(--grey);
border-radius: .5rem;
position: relative;
min-height: 70px;
min-width: 150px;
.name {
font-weight: 700;
}
span {
padding: 2px;
}
.rm {
color: var(--warn);
position: absolute;
display: none;
top: -2px;
transition: all .2s ease;
left: -2px;
transform: translateX(-50%) translateY(-50%);
cursor: pointer;
&[disabled] {
display: none;
}
}
&:not(.disabled) {
cursor: default;
&:hover {
.rm {
display: block;
}
}
}
img {
height: 100%;
width: 100%;
object-fit: scale-down;
}
}
.new-idp:not(.disabled) {
cursor: pointer;
}
}
.divider {
width: 100%;
height: 1px;
@@ -1,15 +1,12 @@
import { Component, Injector, OnDestroy, Type } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { LoginMethodComponentType } from 'src/app/modules/mfa-table/mfa-table.component';
import {
GetLoginPolicyResponse as AdminGetLoginPolicyResponse,
UpdateLoginPolicyRequest,
UpdateLoginPolicyResponse,
} from 'src/app/proto/generated/zitadel/admin_pb';
import { IDP, IDPLoginPolicyLink, IDPOwnerType, IDPStylingType } from 'src/app/proto/generated/zitadel/idp_pb';
import {
AddCustomLoginPolicyRequest,
GetLoginPolicyResponse as MgmtGetLoginPolicyResponse,
@@ -21,7 +18,7 @@ import { ToastService } from 'src/app/services/toast.service';
import { GridPolicy, LOGIN_POLICY } from '../../policy-grid/policies';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
import { AddIdpDialogComponent } from './add-idp-dialog/add-idp-dialog.component';
import { LoginMethodComponentType } from './mfa-table/mfa-table.component';
@Component({
selector: 'app-login-policy',
@@ -37,17 +34,14 @@ export class LoginPolicyComponent implements OnDestroy {
public service!: ManagementService | AdminService;
public PolicyComponentServiceType: any = PolicyComponentServiceType;
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
public idps: IDPLoginPolicyLink.AsObject[] = [];
public loading: boolean = false;
public disabled: boolean = true;
public IDPStylingType: any = IDPStylingType;
public currentPolicy: GridPolicy = LOGIN_POLICY;
constructor(
private route: ActivatedRoute,
private toast: ToastService,
private dialog: MatDialog,
private injector: Injector,
) {
this.sub = this.route.data.pipe(switchMap(data => {
@@ -83,9 +77,7 @@ export class LoginPolicyComponent implements OnDestroy {
this.disabled = this.isDefault;
}
});
this.getIdps().then(resp => {
this.idps = resp;
});
}
public ngOnDestroy(): void {
@@ -102,21 +94,6 @@ export class LoginPolicyComponent implements OnDestroy {
}
}
private async getIdps(): Promise<IDPLoginPolicyLink.AsObject[]> {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
return (this.service as ManagementService).listLoginPolicyIDPs()
.then((resp) => {
return resp.resultList;
});
case PolicyComponentServiceType.ADMIN:
return (this.service as AdminService).listLoginPolicyIDPs()
.then((providers) => {
return providers.resultList;
});
}
}
private async updateData():
Promise<UpdateLoginPolicyResponse.AsObject> {
switch (this.serviceType) {
@@ -172,62 +149,6 @@ export class LoginPolicyComponent implements OnDestroy {
}
}
public openDialog(): void {
const dialogRef = this.dialog.open(AddIdpDialogComponent, {
data: {
serviceType: this.serviceType,
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp && resp.idp && resp.type) {
this.addIdp(resp.idp, resp.type).then(() => {
this.loading = true;
setTimeout(() => {
this.fetchData();
}, 2000);
}).catch(error => {
this.toast.showError(error);
});
}
});
}
private addIdp(idp: IDP.AsObject | IDP.AsObject, ownerType: IDPOwnerType): Promise<any> {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
return (this.service as ManagementService).addIDPToLoginPolicy(idp.id, ownerType);
case PolicyComponentServiceType.ADMIN:
return (this.service as AdminService).addIDPToLoginPolicy(idp.id);
}
}
public removeIdp(idp: IDPLoginPolicyLink.AsObject): void {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
(this.service as ManagementService).removeIDPFromLoginPolicy(idp.idpId).then(() => {
const index = this.idps.findIndex(temp => temp === idp);
if (index > -1) {
this.idps.splice(index, 1);
}
}, error => {
this.toast.showError(error);
});
break;
case PolicyComponentServiceType.ADMIN:
(this.service as AdminService).removeIDPFromLoginPolicy(idp.idpId).then(() => {
const index = this.idps.findIndex(temp => temp === idp);
if (index > -1) {
this.idps.splice(index, 1);
}
}, error => {
this.toast.showError(error);
});
break;
}
}
public get isDefault(): boolean {
if (this.loginData && this.serviceType === PolicyComponentServiceType.MGMT) {
return (this.loginData as LoginPolicy.AsObject).isDefault;
@@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatRippleModule } from '@angular/material/core';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
@@ -14,18 +15,26 @@ import { CardModule } from 'src/app/modules/card/card.module';
import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module';
import { IdpTableModule } from 'src/app/modules/idp-table/idp-table.module';
import { InputModule } from 'src/app/modules/input/input.module';
import { MfaTableModule } from 'src/app/modules/mfa-table/mfa-table.module';
import { HasFeaturePipeModule } from 'src/app/pipes/has-feature-pipe/has-feature-pipe.module';
import { HasRolePipeModule } from 'src/app/pipes/has-role-pipe/has-role-pipe.module';
import { InfoSectionModule } from '../../info-section/info-section.module';
import { PolicyGridModule } from '../../policy-grid/policy-grid.module';
import { AddIdpDialogModule } from './add-idp-dialog/add-idp-dialog.module';
import { AddIdpDialogComponent } from './login-policy-idps/add-idp-dialog/add-idp-dialog.component';
import { LoginPolicyIdpsComponent } from './login-policy-idps/login-policy-idps.component';
import { LoginPolicyRoutingModule } from './login-policy-routing.module';
import { LoginPolicyComponent } from './login-policy.component';
import { DialogAddTypeComponent } from './mfa-table/dialog-add-type/dialog-add-type.component';
import { MfaTableComponent } from './mfa-table/mfa-table.component';
@NgModule({
declarations: [LoginPolicyComponent],
declarations: [
LoginPolicyComponent,
LoginPolicyIdpsComponent,
MfaTableComponent,
DialogAddTypeComponent,
AddIdpDialogComponent,
],
imports: [
LoginPolicyRoutingModule,
CommonModule,
@@ -33,21 +42,20 @@ import { LoginPolicyComponent } from './login-policy.component';
FormsModule,
CardModule,
InputModule,
MatIconModule,
MatButtonModule,
HasFeaturePipeModule,
MatSlideToggleModule,
MatIconModule,
HasRoleModule,
MatDialogModule,
HasRolePipeModule,
MatTooltipModule,
TranslateModule,
DetailLayoutModule,
AddIdpDialogModule,
IdpTableModule,
MfaTableModule,
MatProgressSpinnerModule,
MatSelectModule,
MatRippleModule,
TranslateModule,
PolicyGridModule,
],
})
@@ -0,0 +1,19 @@
<div class="sp_wrapper">
<mat-spinner diameter="30" *ngIf="loading$ | async"></mat-spinner>
</div>
<div class="mfa-list">
<div class="mfa mat-elevation-z1" *ngFor="let mfa of mfas">
<span>
{{(componentType == LoginMethodComponentType.SecondFactor ? 'MFA.SECONDFACTORTYPES.':
LoginMethodComponentType.MultiFactor ? 'MFA.MULTIFACTORTYPES.': '')+mfa | translate}}
</span>
<button color="warn" *ngIf="!disabled" mat-icon-button (click)="removeMfa(mfa)" class="rm">
<mat-icon matTooltip="{{'ACTIONS.REMOVE' | translate}}">remove_circle</mat-icon>
</button>
</div>
<button mat-raised-button color="primary" class="new-mfa" [disabled]="disabled" (click)="!disabled ? addMfa(): null">
<span>{{'ACTIONS.ADD' | translate}}</span>
<mat-icon class="icon">add</mat-icon>
</button>
</div>
@@ -0,0 +1,41 @@
.mfa-list {
display: flex;
flex-direction: column;
.mfa {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
border-radius: .5rem;
margin-bottom: .5rem;
min-height: 40px;
.fill-space {
flex: 1;
}
.rm {
display: none;
transition: all .2s ease;
cursor: pointer;
}
&:hover {
.rm {
display: block;
}
}
}
.new-mfa {
display: flex;
align-items: center;
align-self: flex-end;
margin-top: .5rem;
.icon {
margin-left: .5rem;
}
}
}
@@ -0,0 +1,224 @@
import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatPaginator } from '@angular/material/paginator';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { PolicyComponentServiceType } from 'src/app/modules/policies/policy-component-types.enum';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import {
AddMultiFactorToLoginPolicyRequest as AdminAddMultiFactorToLoginPolicyRequest,
AddSecondFactorToLoginPolicyRequest as AdminAddSecondFactorToLoginPolicyRequest,
RemoveMultiFactorFromLoginPolicyRequest as AdminRemoveMultiFactorFromLoginPolicyRequest,
RemoveSecondFactorFromLoginPolicyRequest as AdminRemoveSecondFactorFromLoginPolicyRequest,
} from 'src/app/proto/generated/zitadel/admin_pb';
import {
AddMultiFactorToLoginPolicyRequest as MgmtAddMultiFactorToLoginPolicyRequest,
AddSecondFactorToLoginPolicyRequest as MgmtAddSecondFactorToLoginPolicyRequest,
RemoveMultiFactorFromLoginPolicyRequest as MgmtRemoveMultiFactorFromLoginPolicyRequest,
RemoveSecondFactorFromLoginPolicyRequest as MgmtRemoveSecondFactorFromLoginPolicyRequest,
} from 'src/app/proto/generated/zitadel/management_pb';
import { MultiFactorType, SecondFactorType } from 'src/app/proto/generated/zitadel/policy_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 { DialogAddTypeComponent } from './dialog-add-type/dialog-add-type.component';
export enum LoginMethodComponentType {
MultiFactor = 1,
SecondFactor = 2,
}
@Component({
selector: 'app-mfa-table',
templateUrl: './mfa-table.component.html',
styleUrls: ['./mfa-table.component.scss'],
})
export class MfaTableComponent implements OnInit {
public LoginMethodComponentType: any = LoginMethodComponentType;
@Input() componentType!: LoginMethodComponentType;
@Input() public serviceType!: PolicyComponentServiceType;
@Input() service!: AdminService | ManagementService;
@Input() disabled: boolean = false;
@ViewChild(MatPaginator) public paginator!: MatPaginator;
public mfas: Array<MultiFactorType | SecondFactorType> = [];
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
public PolicyComponentServiceType: any = PolicyComponentServiceType;
constructor(public translate: TranslateService, private toast: ToastService, private dialog: MatDialog) { }
public ngOnInit(): void {
this.getData();
}
public removeMfa(type: MultiFactorType | SecondFactorType): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'MFA.DELETE.TITLE',
descriptionKey: 'MFA.DELETE.DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
if (this.componentType === LoginMethodComponentType.MultiFactor) {
const req = new MgmtRemoveMultiFactorFromLoginPolicyRequest();
req.setType(type as MultiFactorType);
(this.service as ManagementService).removeMultiFactorFromLoginPolicy(req).then(() => {
this.toast.showInfo('MFA.TOAST.DELETED', true);
this.refreshPageAfterTimout(2000);
});
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
const req = new MgmtRemoveSecondFactorFromLoginPolicyRequest();
req.setType(type as SecondFactorType);
(this.service as ManagementService).removeSecondFactorFromLoginPolicy(req).then(() => {
this.toast.showInfo('MFA.TOAST.DELETED', true);
this.refreshPageAfterTimout(2000);
});
}
} else if (this.serviceType === PolicyComponentServiceType.ADMIN) {
if (this.componentType === LoginMethodComponentType.MultiFactor) {
const req = new AdminRemoveMultiFactorFromLoginPolicyRequest();
req.setType(type as MultiFactorType);
(this.service as AdminService).removeMultiFactorFromLoginPolicy(req).then(() => {
this.toast.showInfo('MFA.TOAST.DELETED', true);
this.refreshPageAfterTimout(2000);
});
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
const req = new AdminRemoveSecondFactorFromLoginPolicyRequest();
req.setType(type as SecondFactorType);
(this.service as AdminService).removeSecondFactorFromLoginPolicy(req).then(() => {
this.toast.showInfo('MFA.TOAST.DELETED', true);
this.refreshPageAfterTimout(2000);
});
}
}
}
});
}
public addMfa(): void {
let selection: any[] = [];
if (this.componentType === LoginMethodComponentType.MultiFactor) {
selection = [MultiFactorType.MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION];
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
selection = [SecondFactorType.SECOND_FACTOR_TYPE_U2F, SecondFactorType.SECOND_FACTOR_TYPE_OTP];
}
this.mfas.forEach(mfa => {
const index = selection.findIndex(sel => sel === mfa);
if (index > -1) {
selection.splice(index, 1);
}
});
const dialogRef = this.dialog.open(DialogAddTypeComponent, {
data: {
title: 'MFA.CREATE.TITLE',
desc: 'MFA.CREATE.DESCRIPTION',
componentType: this.componentType,
types: selection,
},
width: '400px',
});
dialogRef.afterClosed().subscribe((mfaType: MultiFactorType | SecondFactorType) => {
if (mfaType) {
if (this.serviceType === PolicyComponentServiceType.MGMT) {
if (this.componentType === LoginMethodComponentType.MultiFactor) {
const req = new MgmtAddMultiFactorToLoginPolicyRequest();
req.setType(mfaType as MultiFactorType);
(this.service as ManagementService).addMultiFactorToLoginPolicy(req).then(() => {
this.refreshPageAfterTimout(2000);
}).catch(error => {
this.toast.showError(error);
});
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
const req = new MgmtAddSecondFactorToLoginPolicyRequest();
req.setType(mfaType as SecondFactorType);
(this.service as ManagementService).addSecondFactorToLoginPolicy(req).then(() => {
this.refreshPageAfterTimout(2000);
}).catch(error => {
this.toast.showError(error);
});
}
} else if (this.serviceType === PolicyComponentServiceType.ADMIN) {
if (this.componentType === LoginMethodComponentType.MultiFactor) {
const req = new AdminAddMultiFactorToLoginPolicyRequest();
req.setType(mfaType as MultiFactorType);
(this.service as AdminService).addMultiFactorToLoginPolicy(req).then(() => {
this.refreshPageAfterTimout(2000);
}).catch(error => {
this.toast.showError(error);
});
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
const req = new AdminAddSecondFactorToLoginPolicyRequest();
req.setType(mfaType as SecondFactorType);
(this.service as AdminService).addSecondFactorToLoginPolicy(req).then(() => {
this.refreshPageAfterTimout(2000);
}).catch(error => {
this.toast.showError(error);
});
}
}
}
});
}
private async getData(): Promise<void> {
this.loadingSubject.next(true);
if (this.serviceType === PolicyComponentServiceType.MGMT) {
if (this.componentType === LoginMethodComponentType.MultiFactor) {
(this.service as ManagementService).listLoginPolicyMultiFactors().then(resp => {
this.mfas = resp.resultList;
this.loadingSubject.next(false);
}).catch(error => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
(this.service as ManagementService).listLoginPolicySecondFactors().then(resp => {
this.mfas = resp.resultList;
this.loadingSubject.next(false);
}).catch(error => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
}
} else if (this.serviceType === PolicyComponentServiceType.ADMIN) {
if (this.componentType === LoginMethodComponentType.MultiFactor) {
(this.service as AdminService).listLoginPolicyMultiFactors().then(resp => {
this.mfas = resp.resultList;
this.loadingSubject.next(false);
}).catch(error => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
} else if (this.componentType === LoginMethodComponentType.SecondFactor) {
(this.service as AdminService).listLoginPolicySecondFactors().then(resp => {
this.mfas = resp.resultList;
this.loadingSubject.next(false);
}).catch(error => {
this.toast.showError(error);
this.loadingSubject.next(false);
});
}
}
}
public refreshPageAfterTimout(to: number): void {
setTimeout(() => {
this.getData();
}, to);
}
}
@@ -1,180 +1,181 @@
<app-refresh-table [loading]="dataSource?.loading$ | async" (refreshed)="changePage()"
[emitRefreshOnPreviousRoutes]="refreshOnPreviousRoutes" [timestamp]="dataSource?.viewTimestamp"
[dataSize]="dataSource?.totalResult" [selection]="selection">
<cnsl-form-field @appearfade *ngIf="userGrantListSearchKey != undefined" actions class="filtername">
<input class="filterinput" cnslInput (keyup)="applyFilter($event)"
[placeholder]="('USER.GRANTS.FILTER.' + userGrantListSearchKey.toString()) | translate" #input>
</cnsl-form-field>
[emitRefreshOnPreviousRoutes]="refreshOnPreviousRoutes" [timestamp]="dataSource?.viewTimestamp"
[dataSize]="dataSource?.totalResult" [selection]="selection">
<cnsl-form-field @appearfade *ngIf="userGrantListSearchKey != undefined" actions class="filtername">
<input class="filterinput" cnslInput (keyup)="applyFilter($event)"
[placeholder]="('USER.GRANTS.FILTER.' + userGrantListSearchKey.toString()) | translate" #input>
</cnsl-form-field>
<button color="warn" matTooltip="{{'GRANTS.DELETE' | translate}}" class="icon-button" mat-icon-button actions
(click)="deleteGrantSelection()" *ngIf="selection.hasValue() && disableDelete == false">
<i class="las la-trash"></i>
</button>
<a *ngIf="disableWrite == false" matTooltip="{{'GRANTS.ADD' | translate}}" actions color="primary" color="primary"
mat-raised-button [routerLink]="routerLink">
<mat-icon class="icon">add</mat-icon>{{ 'GRANTS.ADD_BTN' | translate }}
</a>
<button color="warn" matTooltip="{{'GRANTS.DELETE' | translate}}" class="icon-button" mat-icon-button actions
(click)="deleteGrantSelection()" *ngIf="selection.hasValue() && disableDelete == false">
<i class="las la-trash"></i>
</button>
<a *ngIf="disableWrite == false" matTooltip="{{'GRANTS.ADD' | translate}}" actions color="primary" color="primary"
mat-raised-button [routerLink]="routerLink">
<mat-icon class="icon">add</mat-icon>{{ 'GRANTS.ADD_BTN' | translate }}
</a>
<div class="table-wrapper">
<table mat-table multiTemplateDataRows class="table" aria-label="Elements" [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox [disabled]="disableWrite" color="primary" (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td class="selection" mat-cell *matCellDef="let row">
<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?.id] : []) | hasRole | async))"
color="primary" (click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null" [checked]="selection.isSelected(row)">
<app-avatar
*ngIf="context !== UserGrantContext.USER && row && row?.displayName && row.firstName && row.lastName"
class="avatar" [name]="row.displayName" [avatarUrl]="row.avatarUrl || ''" [forColor]="row?.preferredLoginName" [size]="32">
</app-avatar>
</mat-checkbox>
</td>
<div class="table-wrapper">
<table mat-table multiTemplateDataRows class="table" aria-label="Elements" [dataSource]="dataSource">
<ng-container matColumnDef="select">
<th class="selection" mat-header-cell *matHeaderCellDef>
<mat-checkbox [disabled]="disableWrite" color="primary" (change)="$event ? masterToggle() : null"
[checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()">
</mat-checkbox>
</th>
<td class="selection" mat-cell *matCellDef="let row">
<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?.id] : []) | hasRole | async))"
color="primary" (click)="$event.stopPropagation()" (change)="$event ? selection.toggle(row) : null"
[checked]="selection.isSelected(row)">
<app-avatar
*ngIf="context !== UserGrantContext.USER && row && row?.displayName && row.firstName && row.lastName"
class="avatar" [name]="row.displayName" [avatarUrl]="row.avatarUrl || ''"
[forColor]="row?.preferredLoginName" [size]="32">
</app-avatar>
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="user">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.USER' | translate }}
<template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="{key: UserGrantListSearchKey.DISPLAY_NAME}"></template>
</th>
<td mat-cell *matCellDef="let grant">
<a class="user" [routerLink]="['/users', grant.userId]">{{grant?.displayName}}</a>
</td>
</ng-container>
<ng-container matColumnDef="org">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.GRANTEDORGDOMAIN' | translate }}
<template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="{key: UserGrantListSearchKey.ORG_NAME}"></template>
</th>
<td mat-cell *matCellDef="let grant">
{{grant.orgName}} </td>
</ng-container>
<ng-container matColumnDef="projectId">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.PROJECTNAME' | translate }}
<template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="{key: UserGrantListSearchKey.PROJECT_NAME}"></template>
</th>
<td mat-cell *matCellDef="let grant">
{{grant.projectName}} </td>
</ng-container>
<ng-container matColumnDef="dates">
<th mat-header-cell *matHeaderCellDef> DATES </th>
<td mat-cell *matCellDef="let grant">
<div class="date-block" *ngIf="grant.details?.creationDate">
<span class="date-sub">{{ 'PROJECT.GRANT.CREATIONDATE' | translate }}:</span>
<span>{{grant.details.creationDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}</span>
</div>
<div class="date-block" *ngIf="grant.details?.changeDate">
<span class="date-sub">{{ 'PROJECT.GRANT.CHANGEDATE' | translate }}</span>
<span>{{grant.details.changeDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}</span>
</div>
</td>
</ng-container>
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{'PROJECT.GRANT.CREATIONDATE' | translate}} </th>
<td mat-cell *matCellDef="let grant">
<span>{{grant.details.creationDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}</span>
</td>
</ng-container>
<ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.CHANGEDATE' | translate }} </th>
<td mat-cell *matCellDef="let grant">
<span>{{grant.details.changeDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}</span>
</td>
</ng-container>
<ng-container matColumnDef="roleNamesList">
<th mat-header-cell *matHeaderCellDef class="role-data">
{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}
<template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="{key: UserGrantListSearchKey.ROLE_KEY}"></template>
</th>
<td mat-cell *matCellDef="let grant; let i = index" class="role-data">
<ng-container
*ngIf="(context === UserGrantContext.USER || context === UserGrantContext.NONE) && (grant.id && grantToEdit !== grant.id) || (grantToEdit !== grant.id)">
<div class="flex-row">
<div class="role">
<span *ngFor="let role of grant.roleKeysList">{{ role }}</span>
</div>
<span class="fill-space"></span>
<button mat-stroked-button *ngIf="grant.id ? grantToEdit !== grant.id : grantToEdit !== grant.id"
[disabled]="disableWrite || !((['user.grant.write$'] | hasRole | async) || ((context === UserGrantContext.OWNED_PROJECT ? ['user.grant.write:' + grant?.projectId] : context === UserGrantContext.GRANTED_PROJECT ? ['user.grant.write:' + grant?.id] : []) | hasRole | async))"
(click)="loadGrantOptions(grant)" matTooltip="{{'ACTIONS.CHANGE' | translate}}">
<i class="las la-edit"></i>
{{'ACTIONS.EDIT' | translate}}
</button>
</div>
</ng-container>
<div class="row-form">
<ng-container
*ngIf="(context === UserGrantContext.OWNED_PROJECT || context === UserGrantContext.USER || context === UserGrantContext.NONE) && grantToEdit == grant.id && loadedProjectId && loadedProjectId === grant.projectId">
<cnsl-form-field class="form-field" appearance="outline">
<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?.id] : []) | hasRole | async))"
(selectionChange)="updateRoles(grant, $event)">
<mat-option *ngFor="let role of projectRoleOptions" [value]="role.key">
{{role.key}}
</mat-option>
</mat-select>
</cnsl-form-field>
<button matTooltip="{{'ACTIONS.CLOSE' | translate}}"
*ngIf="context === UserGrantContext.USER || context === UserGrantContext.NONE" mat-icon-button
(click)="grantToEdit=''">
<mat-icon>close</mat-icon>
</button>
</ng-container>
<ng-container matColumnDef="user">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.USER' | translate }}
<template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="{key: UserGrantListSearchKey.DISPLAY_NAME}"></template>
</th>
<td mat-cell *matCellDef="let grant">
{{grant?.displayName}}</td>
<ng-container
*ngIf="(context === UserGrantContext.GRANTED_PROJECT || context === UserGrantContext.USER || context === UserGrantContext.NONE) && loadedId && loadedId === grant.projectId && grantToEdit == grant.id">
<cnsl-form-field class="form-field" appearance="outline">
<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?.id] : []) | hasRole | async))"
(selectionChange)="updateRoles(grant, $event)">
<mat-option *ngFor="let role of grantRoleOptions" [value]="role">
{{role}}
</mat-option>
</mat-select>
</cnsl-form-field>
<button matTooltip="{{'ACTIONS.CLOSE' | translate}}"
*ngIf="context === UserGrantContext.USER || context === UserGrantContext.NONE" mat-icon-button
(click)="grantToEdit=''">
<mat-icon>close</mat-icon>
</button>
</ng-container>
</div>
</td>
</ng-container>
<ng-container matColumnDef="org">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.GRANTEDORGDOMAIN' | translate }}
<template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="{key: UserGrantListSearchKey.ORG_NAME}"></template>
</th>
<td mat-cell *matCellDef="let grant">
{{grant.orgName}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;">
</tr>
</table>
<ng-container matColumnDef="projectId">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.PROJECTNAME' | translate }}
<template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="{key: UserGrantListSearchKey.PROJECT_NAME}"></template>
</th>
<td mat-cell *matCellDef="let grant">
{{grant.projectName}} </td>
</ng-container>
<ng-container matColumnDef="dates">
<th mat-header-cell *matHeaderCellDef> DATES </th>
<td mat-cell *matCellDef="let grant">
<div class="date-block" *ngIf="grant.details?.creationDate">
<span class="date-sub">{{ 'PROJECT.GRANT.CREATIONDATE' | translate }}:</span>
<span>{{grant.details.creationDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}</span>
</div>
<div class="date-block" *ngIf="grant.details?.changeDate">
<span class="date-sub">{{ 'PROJECT.GRANT.CHANGEDATE' | translate }}</span>
<span>{{grant.details.changeDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}</span>
</div>
</td>
</ng-container>
<ng-container matColumnDef="creationDate">
<th mat-header-cell *matHeaderCellDef> {{'PROJECT.GRANT.CREATIONDATE' | translate}} </th>
<td mat-cell *matCellDef="let grant">
<span>{{grant.details.creationDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}</span>
</td>
</ng-container>
<ng-container matColumnDef="changeDate">
<th mat-header-cell *matHeaderCellDef> {{ 'PROJECT.GRANT.CHANGEDATE' | translate }} </th>
<td mat-cell *matCellDef="let grant">
<span>{{grant.details.changeDate | timestampToDate | localizedDate: 'dd. MMM, HH:mm' }}</span>
</td>
</ng-container>
<ng-container matColumnDef="roleNamesList">
<th mat-header-cell *matHeaderCellDef class="role-data">
{{ 'PROJECT.GRANT.ROLENAMESLIST' | translate }}
<template [ngTemplateOutlet]="templateRef"
[ngTemplateOutletContext]="{key: UserGrantListSearchKey.ROLE_KEY}"></template>
</th>
<td mat-cell *matCellDef="let grant; let i = index" class="role-data">
<ng-container
*ngIf="(context === UserGrantContext.USER || context === UserGrantContext.NONE) && (grant.id && grantToEdit !== grant.id) || (grantToEdit !== grant.id)">
<div class="flex-row">
<div class="role">
<span *ngFor="let role of grant.roleKeysList">{{ role }}</span>
</div>
<span class="fill-space"></span>
<button mat-stroked-button
*ngIf="grant.id ? grantToEdit !== grant.id : grantToEdit !== grant.id"
[disabled]="disableWrite || !((['user.grant.write$'] | hasRole | async) || ((context === UserGrantContext.OWNED_PROJECT ? ['user.grant.write:' + grant?.projectId] : context === UserGrantContext.GRANTED_PROJECT ? ['user.grant.write:' + grant?.id] : []) | hasRole | async))"
(click)="loadGrantOptions(grant)" matTooltip="{{'ACTIONS.CHANGE' | translate}}">
<i class="las la-edit"></i>
{{'ACTIONS.EDIT' | translate}}
</button>
</div>
</ng-container>
<div class="row-form">
<ng-container
*ngIf="(context === UserGrantContext.OWNED_PROJECT || context === UserGrantContext.USER || context === UserGrantContext.NONE) && grantToEdit == grant.id && loadedProjectId && loadedProjectId === grant.projectId">
<cnsl-form-field class="form-field" appearance="outline">
<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?.id] : []) | hasRole | async))"
(selectionChange)="updateRoles(grant, $event)">
<mat-option *ngFor="let role of projectRoleOptions" [value]="role.key">
{{role.key}}
</mat-option>
</mat-select>
</cnsl-form-field>
<button matTooltip="{{'ACTIONS.CLOSE' | translate}}"
*ngIf="context === UserGrantContext.USER || context === UserGrantContext.NONE"
mat-icon-button (click)="grantToEdit=''">
<mat-icon>close</mat-icon>
</button>
</ng-container>
<ng-container
*ngIf="(context === UserGrantContext.GRANTED_PROJECT || context === UserGrantContext.USER || context === UserGrantContext.NONE) && loadedId && loadedId === grant.projectId && grantToEdit == grant.id">
<cnsl-form-field class="form-field" appearance="outline">
<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?.id] : []) | hasRole | async))"
(selectionChange)="updateRoles(grant, $event)">
<mat-option *ngFor="let role of grantRoleOptions" [value]="role">
{{role}}
</mat-option>
</mat-select>
</cnsl-form-field>
<button matTooltip="{{'ACTIONS.CLOSE' | translate}}"
*ngIf="context === UserGrantContext.USER || context === UserGrantContext.NONE"
mat-icon-button (click)="grantToEdit=''">
<mat-icon>close</mat-icon>
</button>
</ng-container>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;">
</tr>
</table>
<div *ngIf="(dataSource.loading$ | async) == false && !dataSource?.totalResult" class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{'GRANTS.EMPTY' | translate}}</span>
</div>
<cnsl-paginator class="paginator" #paginator [timestamp]="dataSource?.viewTimestamp"
[length]="dataSource.totalResult" [pageSize]="INITIAL_PAGE_SIZE" [length]="dataSource.totalResult"
[pageSizeOptions]="[2, 3, 25, 50, 100, 250]" (page)="changePage($event)">
</cnsl-paginator>
<div *ngIf="(dataSource.loading$ | async) == false && !dataSource?.totalResult" class="no-content-row">
<i class="las la-exclamation"></i>
<span>{{'GRANTS.EMPTY' | translate}}</span>
</div>
<cnsl-paginator class="paginator" #paginator [timestamp]="dataSource?.viewTimestamp"
[length]="dataSource.totalResult" [pageSize]="INITIAL_PAGE_SIZE" [length]="dataSource.totalResult"
[pageSizeOptions]="[2, 3, 25, 50, 100, 250]" (page)="changePage($event)">
</cnsl-paginator>
</div>
</app-refresh-table>
<ng-template #templateRef let-key="key">
<button class="search-button" mat-icon-button (click)="setFilter(key)">
<mat-icon class="icon" *ngIf="this.userGrantListSearchKey != key">search</mat-icon>
<mat-icon class="icon" *ngIf="this.userGrantListSearchKey == key">search_off</mat-icon>
</button>
<button class="search-button" mat-icon-button (click)="setFilter(key)">
<mat-icon class="icon" *ngIf="this.userGrantListSearchKey != key">search</mat-icon>
<mat-icon class="icon" *ngIf="this.userGrantListSearchKey == key">search_off</mat-icon>
</button>
</ng-template>
@@ -21,6 +21,11 @@
}
}
.user {
text-decoration: none;
color: inherit;
}
th {
.search-button {
visibility: hidden;