feat: Lockout policy (#2121)

* feat: lock users if lockout policy is set

* feat: setup

* feat: lock user on password failes

* feat: render error

* feat: lock user on command side

* feat: auth_req tests

* feat: lockout policy docs

* feat: remove show lockout failures from proto

* fix: console lockout

* feat: tests

* fix: tests

* unlock function

* add unlock button

* fix migration version

* lockout policy

* lint

* Update internal/auth/repository/eventsourcing/eventstore/auth_request.go

Co-authored-by: Silvan <silvan.reusser@gmail.com>

* fix: err message

* Update internal/command/setup_step4.go

Co-authored-by: Silvan <silvan.reusser@gmail.com>

Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Livio Amstutz <livio.a@gmail.com>
Co-authored-by: Silvan <silvan.reusser@gmail.com>
This commit is contained in:
Fabi
2021-08-11 08:36:32 +02:00
committed by GitHub
parent 272e411e27
commit bc951985ed
101 changed files with 2170 additions and 1574 deletions

View File

@@ -35,6 +35,7 @@
"libphonenumber-js": "^1.9.16",
"moment": "^2.29.1",
"ngx-color": "^7.2.0",
"ngx-image-cropper": "^3.3.5",
"ngx-quicklink": "^0.2.6",
"rxjs": "~6.6.7",
"tinycolor2": "^1.4.2",
@@ -10368,6 +10369,24 @@
"@angular/core": ">=12.0.0-0"
}
},
"node_modules/ngx-image-cropper": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/ngx-image-cropper/-/ngx-image-cropper-3.3.5.tgz",
"integrity": "sha512-0yRVKG5XAbVo3rOaj/iFDlekGsxEqXKU9iXFbjyvHvRT2DFs+AjwtyvINsHCWw+4ed9yA4Y+wLIUNqzA0bfxLw==",
"dependencies": {
"tslib": "^1.9.0"
},
"peerDependencies": {
"@angular/common": ">=8.0.0",
"@angular/core": ">=8.0.0",
"@angular/platform-browser": ">=8.0.0"
}
},
"node_modules/ngx-image-cropper/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/ngx-quicklink": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/ngx-quicklink/-/ngx-quicklink-0.2.7.tgz",
@@ -27536,6 +27555,21 @@
"tslib": "^2.1.0"
}
},
"ngx-image-cropper": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/ngx-image-cropper/-/ngx-image-cropper-3.3.5.tgz",
"integrity": "sha512-0yRVKG5XAbVo3rOaj/iFDlekGsxEqXKU9iXFbjyvHvRT2DFs+AjwtyvINsHCWw+4ed9yA4Y+wLIUNqzA0bfxLw==",
"requires": {
"tslib": "^1.9.0"
},
"dependencies": {
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
"ngx-quicklink": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/ngx-quicklink/-/ngx-quicklink-0.2.7.tgz",

View File

@@ -1,10 +1,10 @@
<app-detail-layout [backRouterLink]="[ serviceType === PolicyComponentServiceType.ADMIN ? '/iam/policies' : '/org']"
[title]="'POLICY.PWD_LOCKOUT.TITLE' | translate" [description]="'POLICY.PWD_LOCKOUT.DESCRIPTION' | translate">
<p class="default" *ngIf="isDefault"> {{'POLICY.DEFAULTLABEL' | translate}}</p>
<cnsl-info-section class="default" *ngIf="isDefault"> {{'POLICY.DEFAULTLABEL' | translate}}</cnsl-info-section>
<ng-template appHasRole [appHasRole]="['policy.delete']">
<button *ngIf="serviceType === PolicyComponentServiceType.MGMT && !isDefault"
matTooltip="{{'POLICY.RESET' | translate}}" color="warn" (click)="removePolicy()" mat-stroked-button>
matTooltip="{{'POLICY.RESET' | translate}}" color="warn" (click)="resetPolicy()" mat-stroked-button>
{{'POLICY.RESET' | translate}}
</button>
</ng-template>
@@ -14,22 +14,15 @@
<span class="left-desc">{{'POLICY.DATA.MAXATTEMPTS' | translate}}</span>
<span class="fill-space"></span>
<div class="length-wrapper">
<button mat-icon-button (click)="incrementMaxAttempts()">
<mat-icon>add</mat-icon>
</button>
<span>{{lockoutData?.maxAttempts}}</span>
<button mat-icon-button (click)="decrementMaxAttempts()">
<mat-icon>remove</mat-icon>
</button>
<span>{{lockoutData?.maxPasswordAttempts}}</span>
<button mat-icon-button (click)="incrementMaxAttempts()">
<mat-icon>add</mat-icon>
</button>
</div>
</div>
<div class="row">
<span class="left-desc">{{'POLICY.DATA.SHOWLOCKOUTFAILURES' | translate}}</span>
<span class="fill-space"></span>
<mat-slide-toggle color="primary" name="showLockoutFailure" ngDefaultControl
[(ngModel)]="lockoutData.showLockoutFailure">
</mat-slide-toggle>
</div>
</div>
<div class="btn-container">

View File

@@ -1,6 +1,6 @@
.default {
color: var(--color-main);
margin-top: 0;
display: block;
margin-bottom: 1rem;
}
.content {

View File

@@ -3,13 +3,11 @@ import { FormGroup } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { GetLockoutPolicyResponse as AdminGetPasswordLockoutPolicyResponse } from 'src/app/proto/generated/zitadel/admin_pb';
import {
GetPasswordLockoutPolicyResponse as AdminGetPasswordLockoutPolicyResponse,
} from 'src/app/proto/generated/zitadel/admin_pb';
import {
GetPasswordLockoutPolicyResponse as MgmtGetPasswordLockoutPolicyResponse,
GetLockoutPolicyResponse as MgmtGetPasswordLockoutPolicyResponse,
} from 'src/app/proto/generated/zitadel/management_pb';
import { PasswordLockoutPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
import { LockoutPolicy } 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';
@@ -17,127 +15,124 @@ import { ToastService } from 'src/app/services/toast.service';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
@Component({
selector: 'app-password-lockout-policy',
templateUrl: './password-lockout-policy.component.html',
styleUrls: ['./password-lockout-policy.component.scss'],
selector: 'app-password-lockout-policy',
templateUrl: './password-lockout-policy.component.html',
styleUrls: ['./password-lockout-policy.component.scss'],
})
export class PasswordLockoutPolicyComponent implements OnDestroy {
@Input() public service!: ManagementService | AdminService;
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
@Input() public service!: ManagementService | AdminService;
public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
public lockoutForm!: FormGroup;
public lockoutData!: PasswordLockoutPolicy.AsObject;
private sub: Subscription = new Subscription();
public PolicyComponentServiceType: any = PolicyComponentServiceType;
public lockoutForm!: FormGroup;
public lockoutData!: LockoutPolicy.AsObject;
private sub: Subscription = new Subscription();
public PolicyComponentServiceType: any = PolicyComponentServiceType;
constructor(
private route: ActivatedRoute,
private toast: ToastService,
private injector: Injector,
) {
this.sub = this.route.data.pipe(switchMap(data => {
this.serviceType = data.serviceType;
constructor(
private route: ActivatedRoute,
private toast: ToastService,
private injector: Injector,
) {
this.sub = this.route.data.pipe(switchMap(data => {
this.serviceType = data.serviceType;
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>);
break;
case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>);
break;
}
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
this.service = this.injector.get(ManagementService as Type<ManagementService>);
break;
case PolicyComponentServiceType.ADMIN:
this.service = this.injector.get(AdminService as Type<AdminService>);
break;
}
return this.route.params;
})).subscribe(() => {
this.fetchData();
return this.route.params;
})).subscribe(() => {
this.fetchData();
});
}
public ngOnDestroy(): void {
this.sub.unsubscribe();
}
private fetchData(): void {
this.getData().then(resp => {
if (resp.policy) {
this.lockoutData = resp.policy;
}
});
}
private getData():
Promise<AdminGetPasswordLockoutPolicyResponse.AsObject | MgmtGetPasswordLockoutPolicyResponse.AsObject> {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
return (this.service as ManagementService).getLockoutPolicy();
case PolicyComponentServiceType.ADMIN:
return (this.service as AdminService).getLockoutPolicy();
}
}
public resetPolicy(): void {
if (this.service instanceof ManagementService) {
this.service.resetLockoutPolicyToDefault().then(() => {
this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
this.fetchData();
}).catch(error => {
this.toast.showError(error);
});
}
}
public incrementMaxAttempts(): void {
if (this.lockoutData?.maxPasswordAttempts !== undefined) {
this.lockoutData.maxPasswordAttempts++;
}
}
public decrementMaxAttempts(): void {
if (this.lockoutData?.maxPasswordAttempts && this.lockoutData?.maxPasswordAttempts > 0) {
this.lockoutData.maxPasswordAttempts--;
}
}
public savePolicy(): void {
let promise: Promise<any>;
if (this.service instanceof AdminService) {
promise = this.service.updateLockoutPolicy(
this.lockoutData.maxPasswordAttempts,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
} else {
if ((this.lockoutData as LockoutPolicy.AsObject).isDefault) {
promise = this.service.addCustomLockoutPolicy(
this.lockoutData.maxPasswordAttempts,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
}
public ngOnDestroy(): void {
this.sub.unsubscribe();
}
private fetchData(): void {
this.getData().then(resp => {
if (resp.policy) {
this.lockoutData = resp.policy;
}
} else {
promise = this.service.updateCustomLockoutPolicy(
this.lockoutData.maxPasswordAttempts,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
}
}
}
private getData():
Promise<AdminGetPasswordLockoutPolicyResponse.AsObject | MgmtGetPasswordLockoutPolicyResponse.AsObject> {
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
return (this.service as ManagementService).getPasswordLockoutPolicy();
case PolicyComponentServiceType.ADMIN:
return (this.service as AdminService).getPasswordLockoutPolicy();
}
}
public removePolicy(): void {
if (this.service instanceof ManagementService) {
this.service.resetPasswordLockoutPolicyToDefault().then(() => {
this.toast.showInfo('POLICY.TOAST.RESETSUCCESS', true);
this.fetchData();
}).catch(error => {
this.toast.showError(error);
});
}
}
public incrementMaxAttempts(): void {
if (this.lockoutData?.maxAttempts !== undefined) {
this.lockoutData.maxAttempts++;
}
}
public decrementMaxAttempts(): void {
if (this.lockoutData?.maxAttempts && this.lockoutData?.maxAttempts > 0) {
this.lockoutData.maxAttempts--;
}
}
public savePolicy(): void {
let promise: Promise<any>;
if (this.service instanceof AdminService) {
promise = this.service.updatePasswordLockoutPolicy(
this.lockoutData.maxAttempts,
this.lockoutData.showLockoutFailure,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
} else {
if ((this.lockoutData as PasswordLockoutPolicy.AsObject).isDefault) {
promise = this.service.addCustomPasswordLockoutPolicy(
this.lockoutData.maxAttempts,
this.lockoutData.showLockoutFailure,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
} else {
promise = this.service.updateCustomPasswordLockoutPolicy(
this.lockoutData.maxAttempts,
this.lockoutData.showLockoutFailure,
).then(() => {
this.toast.showInfo('POLICY.TOAST.SET', true);
}).catch(error => {
this.toast.showError(error);
});
}
}
}
public get isDefault(): boolean {
if (this.lockoutData && this.serviceType === PolicyComponentServiceType.MGMT) {
return (this.lockoutData as PasswordLockoutPolicy.AsObject).isDefault;
} else {
return false;
}
public get isDefault(): boolean {
if (this.lockoutData && this.serviceType === PolicyComponentServiceType.MGMT) {
return (this.lockoutData as LockoutPolicy.AsObject).isDefault;
} else {
return false;
}
}
}

View File

@@ -9,25 +9,26 @@ import { TranslateModule } from '@ngx-translate/core';
import { HasRoleModule } from 'src/app/directives/has-role/has-role.module';
import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module';
import { InputModule } from 'src/app/modules/input/input.module';
import { LinksModule } from '../../links/links.module';
import { InfoSectionModule } from '../../info-section/info-section.module';
import { PasswordLockoutPolicyRoutingModule } from './password-lockout-policy-routing.module';
import { PasswordLockoutPolicyComponent } from './password-lockout-policy.component';
@NgModule({
declarations: [PasswordLockoutPolicyComponent],
imports: [
PasswordLockoutPolicyRoutingModule,
CommonModule,
FormsModule,
InputModule,
MatButtonModule,
MatSlideToggleModule,
MatIconModule,
HasRoleModule,
MatTooltipModule,
TranslateModule,
DetailLayoutModule,
],
declarations: [PasswordLockoutPolicyComponent],
imports: [
PasswordLockoutPolicyRoutingModule,
CommonModule,
FormsModule,
InputModule,
MatButtonModule,
MatSlideToggleModule,
MatIconModule,
HasRoleModule,
MatTooltipModule,
TranslateModule,
DetailLayoutModule,
InfoSectionModule,
],
})
export class PasswordLockoutPolicyModule { }

View File

@@ -25,6 +25,18 @@ export const COMPLEXITY_POLICY: GridPolicy = {
color: 'yellow',
};
export const LOCKOUT_POLICY: GridPolicy = {
i18nTitle: 'POLICY.PWD_LOCKOUT.TITLE',
i18nDesc: 'POLICY.PWD_LOCKOUT.DESCRIPTION',
iamRouterLink: ['/iam', 'policy', PolicyComponentType.LOCKOUT],
orgRouterLink: ['/org', 'policy', PolicyComponentType.LOCKOUT],
iamWithRole: ['iam.policy.read'],
orgWithRole: ['policy.read'],
tags: ['login', 'security'],
icon: 'las la-lock',
color: 'yellow',
};
export const IAM_POLICY = {
i18nTitle: 'POLICY.IAM_POLICY.TITLE',
i18nDesc: 'POLICY.IAM_POLICY.DESCRIPTION',
@@ -99,6 +111,7 @@ export const LOGIN_TEXTS_POLICY = {
export const POLICIES: GridPolicy[] = [
COMPLEXITY_POLICY,
LOCKOUT_POLICY,
IAM_POLICY,
LOGIN_POLICY,
PRIVATELABEL_POLICY,

View File

@@ -17,6 +17,7 @@ import { MemberCreateDialogModule } from 'src/app/modules/add-member-dialog/memb
import { CardModule } from 'src/app/modules/card/card.module';
import { ChangesModule } from 'src/app/modules/changes/changes.module';
import { DetailLayoutModule } from 'src/app/modules/detail-layout/detail-layout.module';
import { InfoSectionModule } from 'src/app/modules/info-section/info-section.module';
import { InputModule } from 'src/app/modules/input/input.module';
import { MachineKeysModule } from 'src/app/modules/machine-keys/machine-keys.module';
import { MetaLayoutModule } from 'src/app/modules/meta-layout/meta-layout.module';
@@ -104,6 +105,7 @@ import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component';
LocalizedDatePipeModule,
InputModule,
MachineKeysModule,
InfoSectionModule,
],
})
export class UserDetailModule { }

View File

@@ -14,6 +14,11 @@
<span class="fill-space"></span>
<ng-template appHasRole [appHasRole]="['user.write$', 'user.write:'+user?.id]">
<button class="unlock-button" mat-stroked-button color="warn"
*ngIf="user?.state === UserState.USER_STATE_LOCKED"
(click)="unlockUser()">{{'USER.PAGES.UNLOCK' |
translate}}</button>
<button class="state-button" mat-stroked-button color="warn"
*ngIf="user?.state !== UserState.USER_STATE_INACTIVE"
(click)="changeState(UserState.USER_STATE_INACTIVE)">{{'USER.PAGES.DEACTIVATE' |
@@ -26,6 +31,7 @@
<mat-progress-bar *ngIf="loading" color="primary" mode="indeterminate"></mat-progress-bar>
<cnsl-info-section class="locked" *ngIf="user?.state === UserState.USER_STATE_LOCKED" type="WARN">{{'USER.PAGES.LOCKEDDESCRIPTION' | translate}}</cnsl-info-section>
<span *ngIf="!loading && !user">{{ 'USER.PAGES.NOUSER' | translate }}</span>
<app-card title="{{ 'USER.PAGES.LOGINNAMES' | translate }}"

View File

@@ -18,11 +18,20 @@
flex: 1;
}
.unlock-button {
margin-left: .5rem;
}
.state-button {
margin-left: .5rem;
}
}
.locked {
display: block;
margin: 1rem 0;
}
.img-phone-email {
width: 300px;
}

View File

@@ -7,7 +7,7 @@ import { take } from 'rxjs/operators';
import { ChangeType } from 'src/app/modules/changes/changes.component';
import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { SendHumanResetPasswordNotificationRequest } from 'src/app/proto/generated/zitadel/management_pb';
import { SendHumanResetPasswordNotificationRequest, UnlockUserRequest } from 'src/app/proto/generated/zitadel/management_pb';
import { Email, Gender, Machine, Phone, Profile, User, UserState } from 'src/app/proto/generated/zitadel/user_pb';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
@@ -16,281 +16,292 @@ import { EditDialogComponent, EditDialogType } from '../auth-user-detail/edit-di
import { ResendEmailDialogComponent } from '../auth-user-detail/resend-email-dialog/resend-email-dialog.component';
@Component({
selector: 'app-user-detail',
templateUrl: './user-detail.component.html',
styleUrls: ['./user-detail.component.scss'],
selector: 'app-user-detail',
templateUrl: './user-detail.component.html',
styleUrls: ['./user-detail.component.scss'],
})
export class UserDetailComponent implements OnInit {
public user!: User.AsObject;
public genders: Gender[] = [Gender.GENDER_MALE, Gender.GENDER_FEMALE, Gender.GENDER_DIVERSE];
public languages: string[] = ['de', 'en'];
public user!: User.AsObject;
public genders: Gender[] = [Gender.GENDER_MALE, Gender.GENDER_FEMALE, Gender.GENDER_DIVERSE];
public languages: string[] = ['de', 'en'];
public ChangeType: any = ChangeType;
public loading: boolean = false;
public ChangeType: any = ChangeType;
public loading: boolean = false;
public UserState: any = UserState;
public copied: string = '';
public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.USER;
public UserState: any = UserState;
public copied: string = '';
public USERGRANTCONTEXT: UserGrantContext = UserGrantContext.USER;
public EditDialogType: any = EditDialogType;
public refreshChanges$: EventEmitter<void> = new EventEmitter();
public EditDialogType: any = EditDialogType;
public refreshChanges$: EventEmitter<void> = new EventEmitter();
constructor(
public translate: TranslateService,
private route: ActivatedRoute,
private toast: ToastService,
public mgmtUserService: ManagementService,
private _location: Location,
private dialog: MatDialog,
private router: Router,
) { }
constructor(
public translate: TranslateService,
private route: ActivatedRoute,
private toast: ToastService,
public mgmtUserService: ManagementService,
private _location: Location,
private dialog: MatDialog,
private router: Router,
) { }
refreshUser(): void {
this.refreshChanges$.emit();
this.route.params.pipe(take(1)).subscribe(params => {
const { id } = params;
this.mgmtUserService.getUserByID(id).then(resp => {
if (resp.user) {
this.user = resp.user;
}
}).catch(err => {
console.error(err);
});
refreshUser(): void {
this.refreshChanges$.emit();
this.route.params.pipe(take(1)).subscribe(params => {
const { id } = params;
this.mgmtUserService.getUserByID(id).then(resp => {
if (resp.user) {
this.user = resp.user;
}
}).catch(err => {
console.error(err);
});
});
}
public ngOnInit(): void {
this.refreshUser();
}
public unlockUser(): void {
const req = new UnlockUserRequest();
req.setId(this.user.id);
this.mgmtUserService.unlockUser(req).then(() => {
this.toast.showInfo('USER.TOAST.UNLOCKED', true);
this.refreshUser();
}).catch(error => {
this.toast.showError(error);
});
}
public changeState(newState: UserState): void {
if (newState === UserState.USER_STATE_ACTIVE) {
this.mgmtUserService.reactivateUser(this.user.id).then(() => {
this.toast.showInfo('USER.TOAST.REACTIVATED', true);
this.user.state = newState;
}).catch(error => {
this.toast.showError(error);
});
} else if (newState === UserState.USER_STATE_INACTIVE) {
this.mgmtUserService.deactivateUser(this.user.id).then(() => {
this.toast.showInfo('USER.TOAST.DEACTIVATED', true);
this.user.state = newState;
}).catch(error => {
this.toast.showError(error);
});
}
}
public saveProfile(profileData: Profile.AsObject): void {
if (this.user.human) {
this.user.human.profile = profileData;
this.mgmtUserService
.updateHumanProfile(
this.user.id,
this.user.human.profile.firstName,
this.user.human.profile.lastName,
this.user.human.profile.nickName,
this.user.human.profile.displayName,
this.user.human.profile.preferredLanguage,
this.user.human.profile.gender)
.then(() => {
this.toast.showInfo('USER.TOAST.SAVED', true);
this.refreshChanges$.emit();
})
.catch(error => {
this.toast.showError(error);
});
}
}
public ngOnInit(): void {
public saveMachine(machineData: Machine.AsObject): void {
if (this.user.machine) {
this.user.machine.name = machineData.name;
this.user.machine.description = machineData.description;
this.mgmtUserService
.updateMachine(
this.user.id,
this.user.machine.name,
this.user.machine.description)
.then(() => {
this.toast.showInfo('USER.TOAST.SAVED', true);
this.refreshChanges$.emit();
})
.catch(error => {
this.toast.showError(error);
});
}
}
public resendEmailVerification(): void {
this.mgmtUserService.resendHumanEmailVerification(this.user.id).then(() => {
this.toast.showInfo('USER.TOAST.EMAILVERIFICATIONSENT', true);
this.refreshChanges$.emit();
}).catch(error => {
this.toast.showError(error);
});
}
public resendPhoneVerification(): void {
this.mgmtUserService.resendHumanPhoneVerification(this.user.id).then(() => {
this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true);
this.refreshChanges$.emit();
}).catch(error => {
this.toast.showError(error);
});
}
public deletePhone(): void {
this.mgmtUserService.removeHumanPhone(this.user.id).then(() => {
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
if (this.user.human) {
this.user.human.phone = new Phone().setPhone('').toObject();
this.refreshUser();
}
}
}).catch(error => {
this.toast.showError(error);
});
}
public changeState(newState: UserState): void {
if (newState === UserState.USER_STATE_ACTIVE) {
this.mgmtUserService.reactivateUser(this.user.id).then(() => {
this.toast.showInfo('USER.TOAST.REACTIVATED', true);
this.user.state = newState;
}).catch(error => {
this.toast.showError(error);
});
} else if (newState === UserState.USER_STATE_INACTIVE) {
this.mgmtUserService.deactivateUser(this.user.id).then(() => {
this.toast.showInfo('USER.TOAST.DEACTIVATED', true);
this.user.state = newState;
}).catch(error => {
this.toast.showError(error);
});
public saveEmail(email: string): void {
if (this.user.id && email) {
this.mgmtUserService.updateHumanEmail(this.user.id, email).then(() => {
this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
if (this.user.state === UserState.USER_STATE_INITIAL) {
this.mgmtUserService.resendHumanInitialization(this.user.id, email ?? '').then(() => {
this.toast.showInfo('USER.TOAST.INITEMAILSENT', true);
this.refreshChanges$.emit();
}).catch(error => {
this.toast.showError(error);
});
}
}
public saveProfile(profileData: Profile.AsObject): void {
if (this.user.human) {
this.user.human.profile = profileData;
this.mgmtUserService
.updateHumanProfile(
this.user.id,
this.user.human.profile.firstName,
this.user.human.profile.lastName,
this.user.human.profile.nickName,
this.user.human.profile.displayName,
this.user.human.profile.preferredLanguage,
this.user.human.profile.gender)
.then(() => {
this.toast.showInfo('USER.TOAST.SAVED', true);
this.refreshChanges$.emit();
})
.catch(error => {
this.toast.showError(error);
});
this.user.human.email = new Email().setEmail(email).toObject();
this.refreshUser();
}
}).catch(error => {
this.toast.showError(error);
});
}
}
public saveMachine(machineData: Machine.AsObject): void {
if (this.user.machine) {
this.user.machine.name = machineData.name;
this.user.machine.description = machineData.description;
this.mgmtUserService
.updateMachine(
this.user.id,
this.user.machine.name,
this.user.machine.description)
.then(() => {
this.toast.showInfo('USER.TOAST.SAVED', true);
this.refreshChanges$.emit();
})
.catch(error => {
this.toast.showError(error);
});
}
}
public resendEmailVerification(): void {
this.mgmtUserService.resendHumanEmailVerification(this.user.id).then(() => {
this.toast.showInfo('USER.TOAST.EMAILVERIFICATIONSENT', true);
this.refreshChanges$.emit();
public savePhone(phone: string): void {
if (this.user.id && phone) {
this.mgmtUserService
.updateHumanPhone(this.user.id, phone).then(() => {
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
if (this.user.human) {
this.user.human.phone = new Phone().setPhone(phone).toObject();
this.refreshUser();
}
}).catch(error => {
this.toast.showError(error);
this.toast.showError(error);
});
}
}
public resendPhoneVerification(): void {
this.mgmtUserService.resendHumanPhoneVerification(this.user.id).then(() => {
this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true);
this.refreshChanges$.emit();
public navigateBack(): void {
this._location.back();
}
public sendSetPasswordNotification(): void {
this.mgmtUserService.sendHumanResetPasswordNotification(
this.user.id,
SendHumanResetPasswordNotificationRequest.Type.TYPE_EMAIL,
).then(() => {
this.toast.showInfo('USER.TOAST.PASSWORDNOTIFICATIONSENT', true);
this.refreshChanges$.emit();
}).catch(error => {
this.toast.showError(error);
});
}
public deleteUser(): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'USER.DIALOG.DELETE_TITLE',
descriptionKey: 'USER.DIALOG.DELETE_DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
this.mgmtUserService.removeUser(this.user.id).then(() => {
const params: Params = {
'deferredReload': true,
};
this.router.navigate(['/users/list', this.user.human ? 'humans' : 'machines'], { queryParams: params });
this.toast.showInfo('USER.TOAST.DELETED', true);
}).catch(error => {
this.toast.showError(error);
this.toast.showError(error);
});
}
}
});
}
public deletePhone(): void {
this.mgmtUserService.removeHumanPhone(this.user.id).then(() => {
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
if (this.user.human) {
this.user.human.phone = new Phone().setPhone('').toObject();
this.refreshUser();
}
public resendInitEmail(): void {
const dialogRef = this.dialog.open(ResendEmailDialogComponent, {
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp.send && this.user.id) {
this.mgmtUserService.resendHumanInitialization(this.user.id, resp.email ?? '').then(() => {
this.toast.showInfo('USER.TOAST.INITEMAILSENT', true);
this.refreshChanges$.emit();
}).catch(error => {
this.toast.showError(error);
this.toast.showError(error);
});
}
}
});
}
public saveEmail(email: string): void {
if (this.user.id && email) {
this.mgmtUserService.updateHumanEmail(this.user.id, email).then(() => {
this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
if (this.user.state === UserState.USER_STATE_INITIAL) {
this.mgmtUserService.resendHumanInitialization(this.user.id, email ?? '').then(() => {
this.toast.showInfo('USER.TOAST.INITEMAILSENT', true);
this.refreshChanges$.emit();
}).catch(error => {
this.toast.showError(error);
});
}
if (this.user.human) {
this.user.human.email = new Email().setEmail(email).toObject();
this.refreshUser();
}
}).catch(error => {
this.toast.showError(error);
});
}
}
public savePhone(phone: string): void {
if (this.user.id && phone) {
this.mgmtUserService
.updateHumanPhone(this.user.id, phone).then(() => {
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
if (this.user.human) {
this.user.human.phone = new Phone().setPhone(phone).toObject();
this.refreshUser();
}
}).catch(error => {
this.toast.showError(error);
});
}
}
public navigateBack(): void {
this._location.back();
}
public sendSetPasswordNotification(): void {
this.mgmtUserService.sendHumanResetPasswordNotification(
this.user.id,
SendHumanResetPasswordNotificationRequest.Type.TYPE_EMAIL,
).then(() => {
this.toast.showInfo('USER.TOAST.PASSWORDNOTIFICATIONSENT', true);
this.refreshChanges$.emit();
}).catch(error => {
this.toast.showError(error);
});
}
public deleteUser(): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'ACTIONS.DELETE',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'USER.DIALOG.DELETE_TITLE',
descriptionKey: 'USER.DIALOG.DELETE_DESCRIPTION',
},
width: '400px',
public openEditDialog(type: EditDialogType): void {
switch (type) {
case EditDialogType.PHONE:
const dialogRefPhone = this.dialog.open(EditDialogComponent, {
data: {
confirmKey: 'ACTIONS.SAVE',
cancelKey: 'ACTIONS.CANCEL',
labelKey: 'ACTIONS.NEWVALUE',
titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE',
descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC',
value: this.user.human?.phone?.phone,
type: EditDialogType.PHONE,
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp) {
this.mgmtUserService.removeUser(this.user.id).then(() => {
const params: Params = {
'deferredReload': true,
};
this.router.navigate(['/users/list', this.user.human ? 'humans' : 'machines'], { queryParams: params });
this.toast.showInfo('USER.TOAST.DELETED', true);
}).catch(error => {
this.toast.showError(error);
});
}
dialogRefPhone.afterClosed().subscribe(resp => {
if (resp) {
this.savePhone(resp);
}
});
}
public resendInitEmail(): void {
const dialogRef = this.dialog.open(ResendEmailDialogComponent, {
width: '400px',
break;
case EditDialogType.EMAIL:
const dialogRefEmail = this.dialog.open(EditDialogComponent, {
data: {
confirmKey: 'ACTIONS.SAVE',
cancelKey: 'ACTIONS.CANCEL',
labelKey: 'ACTIONS.NEWVALUE',
titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE',
descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC',
value: this.user.human?.email?.email,
type: EditDialogType.EMAIL,
},
width: '400px',
});
dialogRef.afterClosed().subscribe(resp => {
if (resp.send && this.user.id) {
this.mgmtUserService.resendHumanInitialization(this.user.id, resp.email ?? '').then(() => {
this.toast.showInfo('USER.TOAST.INITEMAILSENT', true);
this.refreshChanges$.emit();
}).catch(error => {
this.toast.showError(error);
});
}
dialogRefEmail.afterClosed().subscribe(resp => {
if (resp) {
this.saveEmail(resp);
}
});
break;
}
public openEditDialog(type: EditDialogType): void {
switch (type) {
case EditDialogType.PHONE:
const dialogRefPhone = this.dialog.open(EditDialogComponent, {
data: {
confirmKey: 'ACTIONS.SAVE',
cancelKey: 'ACTIONS.CANCEL',
labelKey: 'ACTIONS.NEWVALUE',
titleKey: 'USER.LOGINMETHODS.PHONE.EDITTITLE',
descriptionKey: 'USER.LOGINMETHODS.PHONE.EDITDESC',
value: this.user.human?.phone?.phone,
type: EditDialogType.PHONE,
},
width: '400px',
});
dialogRefPhone.afterClosed().subscribe(resp => {
if (resp) {
this.savePhone(resp);
}
});
break;
case EditDialogType.EMAIL:
const dialogRefEmail = this.dialog.open(EditDialogComponent, {
data: {
confirmKey: 'ACTIONS.SAVE',
cancelKey: 'ACTIONS.CANCEL',
labelKey: 'ACTIONS.NEWVALUE',
titleKey: 'USER.LOGINMETHODS.EMAIL.EDITTITLE',
descriptionKey: 'USER.LOGINMETHODS.EMAIL.EDITDESC',
value: this.user.human?.email?.email,
type: EditDialogType.EMAIL,
},
width: '400px',
});
dialogRefEmail.afterClosed().subscribe(resp => {
if (resp) {
this.saveEmail(resp);
}
});
break;
}
}
}
}

View File

@@ -51,6 +51,8 @@ import {
GetIDPByIDResponse,
GetLabelPolicyRequest,
GetLabelPolicyResponse,
GetLockoutPolicyRequest,
GetLockoutPolicyResponse,
GetLoginPolicyRequest,
GetLoginPolicyResponse,
GetOrgFeaturesRequest,
@@ -61,8 +63,6 @@ import {
GetPasswordAgePolicyResponse,
GetPasswordComplexityPolicyRequest,
GetPasswordComplexityPolicyResponse,
GetPasswordLockoutPolicyRequest,
GetPasswordLockoutPolicyResponse,
GetPreviewLabelPolicyRequest,
GetPreviewLabelPolicyResponse,
GetPrivacyPolicyRequest,
@@ -144,6 +144,8 @@ import {
UpdateIDPResponse,
UpdateLabelPolicyRequest,
UpdateLabelPolicyResponse,
UpdateLockoutPolicyRequest,
UpdateLockoutPolicyResponse,
UpdateLoginPolicyRequest,
UpdateLoginPolicyResponse,
UpdateOrgIAMPolicyRequest,
@@ -152,8 +154,6 @@ import {
UpdatePasswordAgePolicyResponse,
UpdatePasswordComplexityPolicyRequest,
UpdatePasswordComplexityPolicyResponse,
UpdatePasswordLockoutPolicyRequest,
UpdatePasswordLockoutPolicyResponse,
UpdatePrivacyPolicyRequest,
UpdatePrivacyPolicyResponse,
} from '../proto/generated/zitadel/admin_pb';
@@ -431,20 +431,18 @@ export class AdminService {
/* lockout */
public getPasswordLockoutPolicy(): Promise<GetPasswordLockoutPolicyResponse.AsObject> {
const req = new GetPasswordLockoutPolicyRequest();
return this.grpcService.admin.getPasswordLockoutPolicy(req, null).then(resp => resp.toObject());
public getLockoutPolicy(): Promise<GetLockoutPolicyResponse.AsObject> {
const req = new GetLockoutPolicyRequest();
return this.grpcService.admin.getLockoutPolicy(req, null).then(resp => resp.toObject());
}
public updatePasswordLockoutPolicy(
public updateLockoutPolicy(
maxAttempts: number,
showLockoutFailures: boolean,
): Promise<UpdatePasswordLockoutPolicyResponse.AsObject> {
const req = new UpdatePasswordLockoutPolicyRequest();
req.setMaxAttempts(maxAttempts);
req.setShowLockoutFailure(showLockoutFailures);
): Promise<UpdateLockoutPolicyResponse.AsObject> {
const req = new UpdateLockoutPolicyRequest();
req.setMaxPasswordAttempts(maxAttempts);
return this.grpcService.admin.updatePasswordLockoutPolicy(req, null).then(resp => resp.toObject());
return this.grpcService.admin.updateLockoutPolicy(req, null).then(resp => resp.toObject());
}
/* label */

View File

@@ -17,14 +17,14 @@ import {
AddAppKeyResponse,
AddCustomLabelPolicyRequest,
AddCustomLabelPolicyResponse,
AddCustomLockoutPolicyRequest,
AddCustomLockoutPolicyResponse,
AddCustomLoginPolicyRequest,
AddCustomLoginPolicyResponse,
AddCustomPasswordAgePolicyRequest,
AddCustomPasswordAgePolicyResponse,
AddCustomPasswordComplexityPolicyRequest,
AddCustomPasswordComplexityPolicyResponse,
AddCustomPasswordLockoutPolicyRequest,
AddCustomPasswordLockoutPolicyResponse,
AddCustomPrivacyPolicyRequest,
AddCustomPrivacyPolicyResponse,
AddHumanUserRequest,
@@ -122,6 +122,8 @@ import {
GetIAMResponse,
GetLabelPolicyRequest,
GetLabelPolicyResponse,
GetLockoutPolicyRequest,
GetLockoutPolicyResponse,
GetLoginPolicyRequest,
GetLoginPolicyResponse,
GetMyOrgRequest,
@@ -138,8 +140,6 @@ import {
GetPasswordAgePolicyResponse,
GetPasswordComplexityPolicyRequest,
GetPasswordComplexityPolicyResponse,
GetPasswordLockoutPolicyRequest,
GetPasswordLockoutPolicyResponse,
GetPreviewLabelPolicyRequest,
GetPreviewLabelPolicyResponse,
GetPrivacyPolicyRequest,
@@ -298,14 +298,14 @@ import {
ResetCustomVerifyPhoneMessageTextToDefaultResponse,
ResetLabelPolicyToDefaultRequest,
ResetLabelPolicyToDefaultResponse,
ResetLockoutPolicyToDefaultRequest,
ResetLockoutPolicyToDefaultResponse,
ResetLoginPolicyToDefaultRequest,
ResetLoginPolicyToDefaultResponse,
ResetPasswordAgePolicyToDefaultRequest,
ResetPasswordAgePolicyToDefaultResponse,
ResetPasswordComplexityPolicyToDefaultRequest,
ResetPasswordComplexityPolicyToDefaultResponse,
ResetPasswordLockoutPolicyToDefaultRequest,
ResetPasswordLockoutPolicyToDefaultResponse,
ResetPrivacyPolicyToDefaultRequest,
ResetPrivacyPolicyToDefaultResponse,
SendHumanResetPasswordNotificationRequest,
@@ -324,20 +324,22 @@ import {
SetHumanInitialPasswordRequest,
SetPrimaryOrgDomainRequest,
SetPrimaryOrgDomainResponse,
UnlockUserRequest,
UnlockUserResponse,
UpdateAPIAppConfigRequest,
UpdateAPIAppConfigResponse,
UpdateAppRequest,
UpdateAppResponse,
UpdateCustomLabelPolicyRequest,
UpdateCustomLabelPolicyResponse,
UpdateCustomLockoutPolicyRequest,
UpdateCustomLockoutPolicyResponse,
UpdateCustomLoginPolicyRequest,
UpdateCustomLoginPolicyResponse,
UpdateCustomPasswordAgePolicyRequest,
UpdateCustomPasswordAgePolicyResponse,
UpdateCustomPasswordComplexityPolicyRequest,
UpdateCustomPasswordComplexityPolicyResponse,
UpdateCustomPasswordLockoutPolicyRequest,
UpdateCustomPasswordLockoutPolicyResponse,
UpdateCustomPrivacyPolicyRequest,
UpdateCustomPrivacyPolicyResponse,
UpdateHumanEmailRequest,
@@ -556,6 +558,11 @@ export class ManagementService {
return this.grpcService.mgmt.listOrgIDPs(req, null).then(resp => resp.toObject());
}
public unlockUser(req: UnlockUserRequest):
Promise<UnlockUserResponse.AsObject> {
return this.grpcService.mgmt.unlockUser(req, null).then(resp => resp.toObject());
}
public getPrivacyPolicy():
Promise<GetPrivacyPolicyResponse.AsObject> {
const req = new GetPrivacyPolicyRequest();
@@ -1105,36 +1112,31 @@ export class ManagementService {
return this.grpcService.mgmt.updateCustomPasswordComplexityPolicy(req, null).then(resp => resp.toObject());
}
public getPasswordLockoutPolicy(): Promise<GetPasswordLockoutPolicyResponse.AsObject> {
const req = new GetPasswordLockoutPolicyRequest();
return this.grpcService.mgmt.getPasswordLockoutPolicy(req, null).then(resp => resp.toObject());
public getLockoutPolicy(): Promise<GetLockoutPolicyResponse.AsObject> {
const req = new GetLockoutPolicyRequest();
return this.grpcService.mgmt.getLockoutPolicy(req, null).then(resp => resp.toObject());
}
public addCustomPasswordLockoutPolicy(
public addCustomLockoutPolicy(
maxAttempts: number,
showLockoutFailures: boolean,
): Promise<AddCustomPasswordLockoutPolicyResponse.AsObject> {
const req = new AddCustomPasswordLockoutPolicyRequest();
req.setMaxAttempts(maxAttempts);
req.setShowLockoutFailure(showLockoutFailures);
): Promise<AddCustomLockoutPolicyResponse.AsObject> {
const req = new AddCustomLockoutPolicyRequest();
req.setMaxPasswordAttempts(maxAttempts);
return this.grpcService.mgmt.addCustomPasswordLockoutPolicy(req, null).then(resp => resp.toObject());
return this.grpcService.mgmt.addCustomLockoutPolicy(req, null).then(resp => resp.toObject());
}
public resetPasswordLockoutPolicyToDefault(): Promise<ResetPasswordLockoutPolicyToDefaultResponse.AsObject> {
const req = new ResetPasswordLockoutPolicyToDefaultRequest();
return this.grpcService.mgmt.resetPasswordLockoutPolicyToDefault(req, null).then(resp => resp.toObject());
public resetLockoutPolicyToDefault(): Promise<ResetLockoutPolicyToDefaultResponse.AsObject> {
const req = new ResetLockoutPolicyToDefaultRequest();
return this.grpcService.mgmt.resetLockoutPolicyToDefault(req, null).then(resp => resp.toObject());
}
public updateCustomPasswordLockoutPolicy(
public updateCustomLockoutPolicy(
maxAttempts: number,
showLockoutFailures: boolean,
): Promise<UpdateCustomPasswordLockoutPolicyResponse.AsObject> {
const req = new UpdateCustomPasswordLockoutPolicyRequest();
req.setMaxAttempts(maxAttempts);
req.setShowLockoutFailure(showLockoutFailures);
return this.grpcService.mgmt.updateCustomPasswordLockoutPolicy(req, null).then(resp => resp.toObject());
): Promise<UpdateCustomLockoutPolicyResponse.AsObject> {
const req = new UpdateCustomLockoutPolicyRequest();
req.setMaxPasswordAttempts(maxAttempts);
return this.grpcService.mgmt.updateCustomLockoutPolicy(req, null).then(resp => resp.toObject());
}
public getLocalizedComplexityPolicyPatternErrorString(policy: PasswordComplexityPolicy.AsObject): string {

View File

@@ -174,7 +174,9 @@
"REACTIVATE": "Reaktivieren",
"DEACTIVATE": "Deaktivieren",
"FILTER": "Filter",
"DELETE": "Benutzer löschen"
"DELETE": "Benutzer löschen",
"UNLOCK": "Benutzer entsperren",
"LOCKEDDESCRIPTION":"Dieser Benutzer wurde aufgrund der Überschreitung der maximalen Anmeldeversuche gesperrt und muss zur erneuten Verwendung entsperrt werden."
},
"DIALOG": {
"DELETE_TITLE": "User löschen",
@@ -421,7 +423,8 @@
"STATE": {
"0": "Unbekannt",
"1": "Aktiv",
"2": "Abgelaufen"
"2": "Abgelaufen",
"4": "Gesperrt"
},
"SEARCH": {
"FOUND": "Gefunden"
@@ -460,7 +463,8 @@
"SELECTEDKEYSDELETED": "Selektierte Schlüssel gelöscht.",
"KEYADDED": "Schlüssel hinzugefügt!",
"MACHINEADDED": "Service User erstellt!",
"DELETED": "Benutzer erfolgreich gelöscht!"
"DELETED": "Benutzer erfolgreich gelöscht!",
"UNLOCKED":"Benutzer erfolgreich freigeschaltet!"
},
"MEMBERSHIPS": {
"TITLE": "ZITADEL Manager-Rollen",
@@ -688,7 +692,7 @@
},
"PWD_LOCKOUT": {
"TITLE": "Passwortsperre",
"DESCRIPTION": "Standardmässig sind die Passwortwiederholungen bei Falscheingabe nicht begrenzt. Du musst diese Richtlinie installieren, wenn Du Wiederholungsversuche anzeigen, oder eine maximale Anzahl von wiederholten Passworteingaben festlegen möchtest."
"DESCRIPTION": "Lege eine maximale Anzahl an Passwordwiederholungen fest, nachdem Accounts gesperrt werden sollen."
},
"IAM_POLICY": {
"TITLE": "Zugangseinstellungen IAM",

View File

@@ -174,7 +174,9 @@
"REACTIVATE": "Reactivate",
"DEACTIVATE": "Deactivate",
"FILTER": "Filter",
"DELETE": "Delete User"
"DELETE": "Delete User",
"UNLOCK": "Unlock User",
"LOCKEDDESCRIPTION":"This user has been locked out due to exceeding the maximum login attempts and must be unlocked to be used again."
},
"DIALOG": {
"DELETE_TITLE": "Delete User",
@@ -421,7 +423,8 @@
"STATE": {
"0": "Unknown",
"1": "Active",
"2": "Expired"
"2": "Expired",
"4": "Locked"
},
"SEARCH": {
"FOUND": "Found"
@@ -460,7 +463,8 @@
"SELECTEDKEYSDELETED": "Selected keys deleted.",
"KEYADDED": "Key added!",
"MACHINEADDED": "Service User created!",
"DELETED": "User deleted successfully!"
"DELETED": "User deleted successfully!",
"UNLOCKED":"User unlocked successfully!"
},
"MEMBERSHIPS": {
"TITLE": "ZITADEL Manager Roles",
@@ -688,7 +692,7 @@
},
"PWD_LOCKOUT": {
"TITLE": "Password Lockout",
"DESCRIPTION": "Password retries are infinite in default mode. You have to apply this policy if you want to show the number of retries, or set a maximum number of retries after which the account will be blocked."
"DESCRIPTION": "Set a maximum number of passwordretries, after which accounts will be blocked."
},
"IAM_POLICY": {
"TITLE": "IAM Access Preferences",