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:
Fabi
2021-08-11 06:36:32 +00:00
committed by GitHub
co-authored by Silvan Max Peintner Livio Amstutz
parent 272e411e27
commit bc951985ed
101 changed files with 2170 additions and 1574 deletions
@@ -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">
@@ -1,6 +1,6 @@
.default {
color: var(--color-main);
margin-top: 0;
display: block;
margin-bottom: 1rem;
}
.content {
@@ -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;
}
}
}
@@ -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,