mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-31 03:22:15 +00:00
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 <[email protected]> * fix: err message * Update internal/command/setup_step4.go Co-authored-by: Silvan <[email protected]> Co-authored-by: Max Peintner <[email protected]> Co-authored-by: Livio Amstutz <[email protected]> Co-authored-by: Silvan <[email protected]>
This commit is contained in:
co-authored by
Silvan
Max Peintner
Livio Amstutz
parent
272e411e27
commit
bc951985ed
+6
-13
@@ -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">
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
.default {
|
||||
color: var(--color-main);
|
||||
margin-top: 0;
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
|
||||
+111
-116
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-15
@@ -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 { }
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user