Merge branch 'refs/heads/main' into next

This commit is contained in:
Livio Spring 2024-07-24 15:47:02 +02:00
commit 24ab096e13
No known key found for this signature in database
GPG Key ID: 26BB1C2FA5952CF0
119 changed files with 2458 additions and 875 deletions

View File

@ -95,10 +95,8 @@ jobs:
key: ${{ inputs.core_cache_key }}
fail-on-cache-miss: true
-
uses: golangci/golangci-lint-action@v4
uses: golangci/golangci-lint-action@v6
with:
version: ${{ inputs.go_lint_version }}
github-token: ${{ github.token }}
only-new-issues: true
skip-pkg-cache: true
skip-build-cache: true

View File

@ -453,12 +453,10 @@ SystemDefaults:
# Passwords previously hashed with a different algorithm
# or cost are automatically re-hashed using this config,
# upon password validation or update.
# Configure the Hasher config by environment variable using JSON notation:
# ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER='{"Algorithm":"pbkdf2","Rounds":290000,"Hash":"sha256"}'
Hasher:
# Supported algorithms: "argon2i", "argon2id", "bcrypt", "scrypt", "pbkdf2"
# Depending on the algorithm, different configuration options take effect.
Algorithm: bcrypt
Algorithm: bcrypt # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_ALGORITHM
# Cost takes effect for the algorithms bcrypt and scrypt
Cost: 14 # ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_COST
# Time takes effect for the algorithms argon2i and argon2id
@ -498,7 +496,7 @@ SystemDefaults:
Hasher:
# Supported algorithms: "argon2i", "argon2id", "bcrypt", "scrypt", "pbkdf2"
# Depending on the algorithm, different configuration options take effect.
Algorithm: bcrypt
Algorithm: bcrypt # ZITADEL_SYSTEMDEFAULTS_SECRETHASHER_HASHER_ALGORITHM
# Cost takes effect for the algorithms bcrypt and scrypt
Cost: 4 # ZITADEL_SYSTEMDEFAULTS_SECRETHASHER_HASHER_COST
# Time takes effect for the algorithms argon2i and argon2id

View File

@ -18,7 +18,7 @@ var (
shouldReplace bool
)
func New() *cobra.Command {
func New(configFiles *[]string) *cobra.Command {
cmd := &cobra.Command{
Use: "mirror",
Short: "mirrors all data of ZITADEL from one database to another",
@ -37,6 +37,12 @@ Order of execution:
PersistentPreRun: func(cmd *cobra.Command, args []string) {
err := viper.MergeConfig(bytes.NewBuffer(defaultConfig))
logging.OnError(err).Fatal("unable to read default config")
for _, file := range *configFiles {
viper.SetConfigFile(file)
err := viper.MergeInConfig()
logging.WithFields("file", file).OnError(err).Warn("unable to read config file")
}
},
Run: func(cmd *cobra.Command, args []string) {
config := mustNewMigrationConfig(viper.GetViper())

View File

@ -551,15 +551,17 @@ func showBasicInformation(startConfig *Config) {
consoleURL := fmt.Sprintf("%s://%s:%v/ui/console\n", http, startConfig.ExternalDomain, startConfig.ExternalPort)
healthCheckURL := fmt.Sprintf("%s://%s:%v/debug/healthz\n", http, startConfig.ExternalDomain, startConfig.ExternalPort)
machineIdMethod := id.MachineIdentificationMethod()
insecure := !startConfig.TLS.Enabled && !startConfig.ExternalSecure
fmt.Printf(" ===============================================================\n\n")
fmt.Printf(" Version : %s\n", build.Version())
fmt.Printf(" TLS enabled : %v\n", startConfig.TLS.Enabled)
fmt.Printf(" External Secure : %v\n", startConfig.ExternalSecure)
fmt.Printf(" Console URL : %s", color.BlueString(consoleURL))
fmt.Printf(" Health Check URL : %s", color.BlueString(healthCheckURL))
fmt.Printf(" Version : %s\n", build.Version())
fmt.Printf(" TLS enabled : %v\n", startConfig.TLS.Enabled)
fmt.Printf(" External Secure : %v\n", startConfig.ExternalSecure)
fmt.Printf(" Machine Id Method : %v\n", machineIdMethod)
fmt.Printf(" Console URL : %s", color.BlueString(consoleURL))
fmt.Printf(" Health Check URL : %s", color.BlueString(healthCheckURL))
if insecure {
fmt.Printf("\n %s: you're using plain http without TLS. Be aware this is \n", color.RedString("Warning"))
fmt.Printf(" not a secure setup and should only be used for test systems. \n")

View File

@ -56,7 +56,7 @@ func New(out io.Writer, in io.Reader, args []string, server chan<- *start.Server
start.New(server),
start.NewStartFromInit(server),
start.NewStartFromSetup(server),
mirror.New(),
mirror.New(&configFiles),
key.New(),
ready.New(),
)

View File

@ -1,95 +0,0 @@
import { Component, Inject } from '@angular/core';
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { Duration } from 'google-protobuf/google/protobuf/duration_pb';
import { requiredValidator } from 'src/app/modules/form-field/validators/validators';
import { UpdateSecretGeneratorRequest } from 'src/app/proto/generated/zitadel/admin_pb';
@Component({
selector: 'cnsl-dialog-add-secret-generator',
templateUrl: './dialog-add-secret-generator.component.html',
styleUrls: ['./dialog-add-secret-generator.component.scss'],
})
export class DialogAddSecretGeneratorComponent {
public req: UpdateSecretGeneratorRequest = new UpdateSecretGeneratorRequest();
public specsForm!: UntypedFormGroup;
constructor(
private fb: UntypedFormBuilder,
public dialogRef: MatDialogRef<DialogAddSecretGeneratorComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
) {
let exp = 1;
if (data.config?.expiry !== undefined) {
exp = this.durationToHour(data.config?.expiry);
}
this.specsForm = this.fb.group({
generatorType: [data.type, [requiredValidator]],
expiry: [exp, [requiredValidator]],
length: [data.config?.length ?? 6, [requiredValidator]],
includeDigits: [data.config?.includeDigits ?? true, [requiredValidator]],
includeSymbols: [data.config?.includeSymbols ?? true, [requiredValidator]],
includeLowerLetters: [data.config?.includeLowerLetters ?? true, [requiredValidator]],
includeUpperLetters: [data.config?.includeUpperLetters ?? true, [requiredValidator]],
});
}
public closeDialog(): void {
this.dialogRef.close();
}
public closeDialogWithRequest(): void {
this.req.setGeneratorType(this.generatorType?.value);
this.req.setExpiry(this.hourToDuration(this.expiry?.value));
this.req.setIncludeDigits(this.includeDigits?.value);
this.req.setIncludeLowerLetters(this.includeLowerLetters?.value);
this.req.setIncludeSymbols(this.includeSymbols?.value);
this.req.setIncludeUpperLetters(this.includeUpperLetters?.value);
this.req.setLength(this.length?.value);
this.dialogRef.close(this.req);
}
public get generatorType(): AbstractControl | null {
return this.specsForm.get('generatorType');
}
public get expiry(): AbstractControl | null {
return this.specsForm.get('expiry');
}
public get includeDigits(): AbstractControl | null {
return this.specsForm.get('includeDigits');
}
public get includeLowerLetters(): AbstractControl | null {
return this.specsForm.get('includeLowerLetters');
}
public get includeSymbols(): AbstractControl | null {
return this.specsForm.get('includeSymbols');
}
public get includeUpperLetters(): AbstractControl | null {
return this.specsForm.get('includeUpperLetters');
}
public get length(): AbstractControl | null {
return this.specsForm.get('length');
}
private durationToHour(duration: Duration.AsObject): number {
if (duration.seconds === 0) {
return 0;
}
return (duration.seconds + duration.nanos / 1000000) / 3600;
}
private hourToDuration(hour: number): Duration {
const exp = hour * 60 * 60;
const sec = Math.floor(exp);
const nanos = Math.round((exp - sec) * 1000000);
return new Duration().setSeconds(sec).setNanos(nanos);
}
}

View File

@ -1,29 +1,36 @@
<h1 mat-dialog-title class="title">
<span>{{ 'SETTING.SECRETS.ADDGENERATOR' | translate }}</span>
</h1>
<div mat-dialog-content>
<form *ngIf="specsForm" (ngSubmit)="closeDialogWithRequest()" [formGroup]="specsForm">
<h2 class="generator-type">{{ 'SETTING.SECRETS.TYPE.' + generatorType?.value | translate }}</h2>
<cnsl-card title="{{ 'SETTING.SECRETS.TYPE.' + generatorType | translate }}">
<form *ngIf="specsForm" [formGroup]="specsForm" (ngSubmit)="saveSecretGenerator()">
<cnsl-form-field class="generator-form-field" label="Expiration">
<cnsl-label>{{ 'SETTING.SECRETS.EXPIRY' | translate }}</cnsl-label>
<input cnslInput name="expiry" type="number" formControlName="expiry" />
<input cnslInput name="expiry" type="number" formControlName="expiry" id="{{ 'expiry' + generatorType }}" />
</cnsl-form-field>
<cnsl-form-field class="generator-form-field" label="Length">
<cnsl-label>{{ 'SETTING.SECRETS.LENGTH' | translate }}</cnsl-label>
<input cnslInput name="length" type="number" formControlName="length" />
<input cnslInput name="length" type="number" formControlName="length" id="{{ 'length' + generatorType }}" />
</cnsl-form-field>
<div class="slide-toggle-wrapper">
<mat-slide-toggle class="slide-toggle" color="primary" name="includeDigits" formControlName="includeDigits">
<mat-slide-toggle
class="slide-toggle"
color="primary"
name="includeDigits"
formControlName="includeDigits"
id="{{ 'includeDigits' + generatorType }}"
>
<div class="slide-toggle-row">
<mat-icon class="icon" svgIcon="mdi_numeric"></mat-icon>
<span class="left-desc">{{ 'SETTING.SECRETS.INCLUDEDIGITS' | translate }}</span>
</div>
</mat-slide-toggle>
<mat-slide-toggle class="slide-toggle" color="primary" name="includeSymbols" formControlName="includeSymbols">
<mat-slide-toggle
class="slide-toggle"
color="primary"
name="includeSymbols"
formControlName="includeSymbols"
id="{{ 'includeSymbols' + generatorType }}"
>
<div class="slide-toggle-row">
<mat-icon class="icon" svgIcon="mdi_symbol"></mat-icon>
<span class="left-desc">{{ 'SETTING.SECRETS.INCLUDESYMBOLS' | translate }}</span>
@ -35,6 +42,7 @@
color="primary"
name="includeLowerLetters"
formControlName="includeLowerLetters"
id="{{ 'includeLowerLetters' + generatorType }}"
>
<div class="slide-toggle-row">
<mat-icon class="icon" svgIcon="mdi_format-letter-case-lower"></mat-icon>
@ -47,6 +55,7 @@
color="primary"
name="includeUpperLetters"
formControlName="includeUpperLetters"
id="{{ 'includeUpperLetters' + generatorType }}"
>
<div class="slide-toggle-row">
<mat-icon class="icon" svgIcon="mdi_format-letter-case-upper"></mat-icon>
@ -55,18 +64,17 @@
</mat-slide-toggle>
</div>
</form>
</div>
<div mat-dialog-actions class="action">
<button mat-stroked-button (click)="closeDialog()">
<span>{{ 'ACTIONS.CLOSE' | translate }}</span>
</button>
<button
[disabled]="!specsForm.valid"
mat-raised-button
class="ok-button"
color="primary"
(click)="closeDialogWithRequest()"
>
<span>{{ 'ACTIONS.SAVE' | translate }}</span>
</button>
</div>
<div class="action">
<button
(click)="saveSecretGenerator()"
[disabled]="!specsForm.valid || specsForm.pristine || loading"
mat-raised-button
color="primary"
type="submit"
id="{{ 'saveSecretGenerator' + generatorType }}"
>
<mat-spinner diameter="20" *ngIf="loading"></mat-spinner>
<span *ngIf="!loading">{{ 'ACTIONS.SAVE' | translate }}</span>
</button>
</div>
</cnsl-card>

View File

@ -34,10 +34,12 @@
}
.action {
display: flex;
justify-content: space-between;
display: inline !important;
button {
border-radius: 0.5rem;
float: right;
margin-top: 0.5rem;
display: flex;
}
}

View File

@ -0,0 +1,135 @@
import { Component, Input, OnInit } from '@angular/core';
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Duration } from 'google-protobuf/google/protobuf/duration_pb';
import { requiredValidator } from 'src/app/modules/form-field/validators/validators';
import { GetSecretGeneratorRequest, UpdateSecretGeneratorRequest } from 'src/app/proto/generated/zitadel/admin_pb';
import { SecretGeneratorType } from 'src/app/proto/generated/zitadel/settings_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ToastService } from 'src/app/services/toast.service';
const MIN_EXPIRATION_IN_MINUTES = 5;
const MIN_LENGTH = 6;
@Component({
selector: 'cnsl-secret-generator-card',
templateUrl: './secret-generator-card.component.html',
styleUrls: ['./secret-generator-card.component.scss'],
})
export class SecretGeneratorCardComponent implements OnInit {
@Input({ required: true }) generatorType!: SecretGeneratorType;
public specsForm!: UntypedFormGroup;
public loading: boolean = false;
ngOnInit() {
this.fetchData();
}
constructor(
private fb: UntypedFormBuilder,
private service: AdminService,
private toast: ToastService,
) {
this.specsForm = this.fb.group({
expiry: [MIN_EXPIRATION_IN_MINUTES, [requiredValidator]],
length: [MIN_LENGTH, [requiredValidator]],
includeDigits: [false, [requiredValidator]],
includeSymbols: [false, [requiredValidator]],
includeLowerLetters: [false, [requiredValidator]],
includeUpperLetters: [false, [requiredValidator]],
});
}
private fetchData(): void {
this.loading = true;
const req = new GetSecretGeneratorRequest();
req.setGeneratorType(this.generatorType);
this.service
.getSecretGenerator(req)
.then((resp) => {
let generator = resp.secretGenerator;
if (generator) {
this.specsForm.patchValue({
length: generator.length,
includeDigits: generator.includeDigits,
includeSymbols: generator.includeSymbols,
includeLowerLetters: generator.includeLowerLetters,
includeUpperLetters: generator.includeUpperLetters,
});
if (generator.expiry !== undefined) {
this.specsForm.patchValue({ expiry: this.durationToMinutes(generator.expiry) });
}
this.specsForm.markAsPristine();
this.loading = false;
}
})
.catch((error) => {
this.toast.showError(error);
this.loading = false;
});
}
public saveSecretGenerator() {
const req = new UpdateSecretGeneratorRequest();
req.setGeneratorType(this.generatorType);
req.setExpiry(this.minutesToDuration(this.expiry?.value));
req.setIncludeDigits(this.includeDigits?.value);
req.setIncludeLowerLetters(this.includeLowerLetters?.value);
req.setIncludeSymbols(this.includeSymbols?.value);
req.setIncludeUpperLetters(this.includeUpperLetters?.value);
req.setLength(this.length?.value);
this.loading = true;
this.service
.updateSecretGenerator(req)
.then(() => {
this.toast.showInfo('SETTING.SECRETS.UPDATED', true);
this.fetchData();
})
.catch((error) => {
this.toast.showError(error);
this.loading = false;
});
}
public get expiry(): AbstractControl | null {
return this.specsForm.get('expiry');
}
public get includeDigits(): AbstractControl | null {
return this.specsForm.get('includeDigits');
}
public get includeLowerLetters(): AbstractControl | null {
return this.specsForm.get('includeLowerLetters');
}
public get includeSymbols(): AbstractControl | null {
return this.specsForm.get('includeSymbols');
}
public get includeUpperLetters(): AbstractControl | null {
return this.specsForm.get('includeUpperLetters');
}
public get length(): AbstractControl | null {
return this.specsForm.get('length');
}
private durationToMinutes(duration: Duration.AsObject): number {
if (duration.seconds === 0) {
return 0;
}
return (duration.seconds + duration.nanos / 1000000000) / 60;
}
private minutesToDuration(minutes: number): Duration {
const exp = minutes * 60;
const sec = Math.floor(exp);
const nanos = Math.round((exp - sec) * 1000000000);
return new Duration().setSeconds(sec).setNanos(nanos);
}
}

View File

@ -1,16 +1,6 @@
<div class="spinner-wr">
<mat-spinner diameter="30" *ngIf="loading" color="primary"></mat-spinner>
</div>
<h2>{{ 'DESCRIPTIONS.SETTINGS.SECRET_GENERATORS.TITLE' | translate }}</h2>
<p class="cnsl-secondary-text">{{ 'DESCRIPTIONS.SETTINGS.SECRET_GENERATORS.DESCRIPTION' | translate }}</p>
<div class="generators">
<cnsl-card class="generator" [nomargin]="true" *ngFor="let gen of AVAILABLEGENERATORS">
<div class="row">
<h3 class="title">{{ 'SETTING.SECRETS.TYPE.' + gen | translate }}</h3>
<button mat-icon-button (click)="openGeneratorDialog(gen)" [disabled]="(['iam.write'] | hasRole | async) === false">
<i class="las la-pen"></i>
</button>
</div>
</cnsl-card>
<cnsl-secret-generator-card *ngFor="let type of AVAILABLEGENERATORS" [generatorType]="type" />
</div>

View File

@ -13,17 +13,4 @@
@media only screen and (max-width: 600px) {
grid-template-columns: 1fr;
}
.generator {
.row {
display: flex;
align-items: center;
justify-content: space-between;
.title {
font-size: 1rem;
margin: 0;
}
}
}
}

View File

@ -1,22 +1,13 @@
import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { UpdateSecretGeneratorRequest } from 'src/app/proto/generated/zitadel/admin_pb';
import { OIDCSettings, SecretGenerator, SecretGeneratorType } from 'src/app/proto/generated/zitadel/settings_pb';
import { AdminService } from 'src/app/services/admin.service';
import { ToastService } from 'src/app/services/toast.service';
import { DialogAddSecretGeneratorComponent } from './dialog-add-secret-generator/dialog-add-secret-generator.component';
import { Component } from '@angular/core';
import { SecretGenerator, SecretGeneratorType } from 'src/app/proto/generated/zitadel/settings_pb';
@Component({
selector: 'cnsl-secret-generator',
templateUrl: './secret-generator.component.html',
styleUrls: ['./secret-generator.component.scss'],
})
export class SecretGeneratorComponent implements OnInit {
export class SecretGeneratorComponent {
public generators: SecretGenerator.AsObject[] = [];
public oidcSettings!: OIDCSettings.AsObject;
public loading: boolean = false;
public readonly AVAILABLEGENERATORS: SecretGeneratorType[] = [
SecretGeneratorType.SECRET_GENERATOR_TYPE_INIT_CODE,
@ -29,58 +20,5 @@ export class SecretGeneratorComponent implements OnInit {
SecretGeneratorType.SECRET_GENERATOR_TYPE_OTP_EMAIL,
];
constructor(
private service: AdminService,
private toast: ToastService,
private dialog: MatDialog,
) {}
ngOnInit(): void {
this.fetchData();
}
private fetchData(): void {
this.service
.listSecretGenerators()
.then((generators) => {
if (generators.resultList) {
this.generators = generators.resultList;
}
})
.catch((error) => {
if (error.code === 5) {
} else {
this.toast.showError(error);
}
});
}
public openGeneratorDialog(generatorType: SecretGeneratorType): void {
let config = this.generators.find((gen) => gen.generatorType === generatorType);
const dialogRef = this.dialog.open(DialogAddSecretGeneratorComponent, {
data: {
type: generatorType,
config: config,
},
width: '400px',
});
dialogRef.afterClosed().subscribe((req: UpdateSecretGeneratorRequest) => {
if (req) {
return (this.service as AdminService)
.updateSecretGenerator(req)
.then(() => {
this.toast.showInfo('SETTING.SECRETS.UPDATED', true);
setTimeout(() => {
this.fetchData();
}, 2000);
})
.catch((error) => {
this.toast.showError(error);
});
} else {
return;
}
});
}
constructor() {}
}

View File

@ -12,11 +12,11 @@ import { MatDialogModule } from '@angular/material/dialog';
import { CardModule } from '../../card/card.module';
import { FormFieldModule } from '../../form-field/form-field.module';
import { InputModule } from '../../input/input.module';
import { DialogAddSecretGeneratorComponent } from './dialog-add-secret-generator/dialog-add-secret-generator.component';
import { SecretGeneratorComponent } from './secret-generator.component';
import { SecretGeneratorCardComponent } from './secret-generator-card/secret-generator-card.component';
@NgModule({
declarations: [SecretGeneratorComponent, DialogAddSecretGeneratorComponent],
declarations: [SecretGeneratorComponent, SecretGeneratorCardComponent],
imports: [
CommonModule,
MatIconModule,
@ -33,6 +33,6 @@ import { SecretGeneratorComponent } from './secret-generator.component';
TranslateModule,
MatDialogModule,
],
exports: [SecretGeneratorComponent, DialogAddSecretGeneratorComponent],
exports: [SecretGeneratorComponent],
})
export class SecretGeneratorModule {}

View File

@ -3,7 +3,7 @@
(closed)="close()"
>
<div class="identity-provider-create-content">
<div class="title-row">
<div class="identity-provider-title-row">
<img class="idp-logo apple dark" src="./assets/images/idp/apple-dark.svg" alt="apple" />
<img class="idp-logo apple light" src="./assets/images/idp/apple.svg" alt="apple" />
<h1>{{ 'IDP.CREATE.APPLE.TITLE' | translate }}</h1>

View File

@ -3,7 +3,7 @@
(closed)="close()"
>
<div class="identity-provider-create-content">
<div class="title-row">
<div class="identity-provider-title-row">
<img class="idp-logo" src="./assets/images/idp/ms.svg" alt="microsoft" />
<h1>{{ 'IDP.CREATE.AZUREAD.TITLE' | translate }}</h1>
<ng-container *ngIf="exists$ | async">

View File

@ -3,7 +3,7 @@
(closed)="close()"
>
<div class="identity-provider-create-content">
<div class="title-row">
<div class="identity-provider-title-row">
<img class="idp-logo dark" src="./assets/images/idp/github-dark.svg" alt="github" />
<img class="idp-logo light" src="./assets/images/idp/github.svg" alt="github" />
<h1>{{ 'IDP.CREATE.GITHUBES.TITLE' | translate }}</h1>

View File

@ -3,7 +3,7 @@
(closed)="close()"
>
<div class="identity-provider-create-content">
<div class="title-row">
<div class="identity-provider-title-row">
<img class="idp-logo dark" src="./assets/images/idp/github-dark.svg" alt="github" />
<img class="idp-logo light" src="./assets/images/idp/github.svg" alt="github" />
<h1>{{ 'IDP.CREATE.GITHUB.TITLE' | translate }}</h1>

View File

@ -3,7 +3,7 @@
(closed)="close()"
>
<div class="identity-provider-create-content">
<div class="title-row">
<div class="identity-provider-title-row">
<img class="idp-logo" src="./assets/images/idp/gitlab.svg" alt="gitlab" />
<h1>{{ 'IDP.CREATE.GITLABSELFHOSTED.TITLE' | translate }}</h1>
<ng-container *ngIf="exists$ | async">

View File

@ -3,7 +3,7 @@
(closed)="close()"
>
<div class="identity-provider-create-content">
<div class="title-row">
<div class="identity-provider-title-row">
<img class="idp-logo" src="./assets/images/idp/gitlab.svg" alt="gitlab" />
<h1>{{ 'IDP.CREATE.GITLAB.TITLE' | translate }}</h1>
<ng-container *ngIf="exists$ | async">

View File

@ -3,7 +3,7 @@
(closed)="close()"
>
<div class="identity-provider-create-content">
<div class="title-row">
<div class="identity-provider-title-row">
<img class="idp-logo" src="./assets/images/idp/google.png" alt="google" />
<h1>{{ 'IDP.CREATE.GOOGLE.TITLE' | translate }}</h1>
<ng-container *ngIf="exists$ | async">

View File

@ -3,7 +3,7 @@
(closed)="close()"
>
<div class="identity-provider-create-content">
<div class="title-row">
<div class="identity-provider-title-row">
<mat-icon class="idp-logo" svgIcon="mdi_jwt" alt="jwt" />
<h1>{{ 'IDP.CREATE.JWT.TITLE' | translate }}</h1>
<ng-container *ngIf="exists$ | async">

View File

@ -3,7 +3,7 @@
(closed)="close()"
>
<div class="identity-provider-create-content">
<div class="title-row">
<div class="identity-provider-title-row">
<i class="idp-icon las la-building"></i>
<h1>{{ 'IDP.CREATE.LDAP.TITLE' | translate }}</h1>
<ng-container *ngIf="exists$ | async">

View File

@ -5,7 +5,7 @@
*ngIf="configureProvider || autofillLink || activateLink"
>
<cnsl-info-section *ngIf="activateLink">
<div class="title-row">
<div class="identity-provider-title-row">
<div class="left">
<h2 class="title">{{ 'DESCRIPTIONS.SETTINGS.IDPS.ACTIVATE.TITLE' | translate }}</h2>
<div>

View File

@ -3,7 +3,7 @@
(closed)="close()"
>
<div class="identity-provider-create-content">
<div class="title-row">
<div class="identity-provider-title-row">
<img class="idp-logo" src="./assets/images/idp/oauth.svg" alt="oauth" />
<h1>{{ 'IDP.CREATE.OAUTH.TITLE' | translate }}</h1>
<ng-container *ngIf="exists$ | async">

View File

@ -3,7 +3,7 @@
(closed)="close()"
>
<div class="identity-provider-create-content">
<div class="title-row">
<div class="identity-provider-title-row">
<mat-icon class="idp-logo" svgIcon="mdi_openid" alt="openid" />
<h1>{{ 'IDP.CREATE.OIDC.TITLE' | translate }}</h1>
<ng-container *ngIf="exists$ | async">

View File

@ -3,7 +3,7 @@
(closed)="close()"
>
<div class="identity-provider-create-content">
<div class="title-row">
<div class="identity-provider-title-row">
<img class="idp-logo" src="./assets/images/idp/saml-icon.svg" alt="saml" />
<h1>{{ 'IDP.CREATE.SAML.TITLE' | translate }}</h1>
<ng-container *ngIf="exists$ | async">

View File

@ -10,7 +10,7 @@
}
.identity-provider-create-content {
.title-row {
.identity-provider-title-row {
display: flex;
align-items: center;

View File

@ -26,6 +26,8 @@ export enum UserTarget {
EXTERNAL = 'external',
}
const USER_LIMIT = 25;
@Component({
selector: 'cnsl-search-user-autocomplete',
templateUrl: './search-user-autocomplete.component.html',
@ -67,9 +69,9 @@ export class SearchUserAutocompleteComponent implements OnInit, AfterContentChec
// feat-3916 show users as soon as I am in the input field of the user
const query = new SearchQuery();
const lnQuery = new LoginNameQuery();
lnQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
lnQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_STARTS_WITH_IGNORE_CASE);
query.setLoginNameQuery(lnQuery);
this.userService.listUsers(10, 0, [query]).then((users) => {
this.userService.listUsers(USER_LIMIT, 0, [query]).then((users) => {
this.filteredUsers = users.resultList;
});
@ -97,7 +99,7 @@ export class SearchUserAutocompleteComponent implements OnInit, AfterContentChec
query.setLoginNameQuery(lnQuery);
if (this.target === UserTarget.SELF) {
return from(this.userService.listUsers(10, 0, [query]));
return from(this.userService.listUsers(USER_LIMIT, 0, [query]));
} else {
return of();
}

View File

@ -18,7 +18,7 @@ import { PageEvent, PaginatorComponent } from '../paginator/paginator.component'
import { UserGrantRoleDialogComponent } from '../user-grant-role-dialog/user-grant-role-dialog.component';
import { WarnDialogComponent } from '../warn-dialog/warn-dialog.component';
import { UserGrantContext, UserGrantsDataSource } from './user-grants-datasource';
import { Org, OrgState } from 'src/app/proto/generated/zitadel/org_pb';
import { Org, OrgIDQuery, OrgQuery, OrgState } from 'src/app/proto/generated/zitadel/org_pb';
export enum UserGrantListSearchKey {
DISPLAY_NAME,
@ -306,17 +306,15 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
}
}
public showUser(grant: UserGrant.AsObject) {
const org: Org.AsObject = {
id: grant.grantedOrgId,
name: grant.grantedOrgName,
state: OrgState.ORG_STATE_ACTIVE,
primaryDomain: grant.grantedOrgDomain,
};
public async showUser(grant: UserGrant.AsObject) {
const orgQuery = new OrgQuery();
const orgIdQuery = new OrgIDQuery();
orgIdQuery.setId(grant.grantedOrgId);
orgQuery.setIdQuery(orgIdQuery);
// Check if user has permissions for that org before changing active org
if (this.myOrgs.find((org) => org.id === grant.grantedOrgId)) {
this.authService.setActiveOrg(org);
const orgs = (await this.authService.listMyProjectOrgs(1, 0, [orgQuery])).resultList;
if (orgs.length === 1) {
this.authService.setActiveOrg(orgs[0]);
this.router.navigate(['/users', grant.userId]);
} else {
this.toast.showInfo('GRANTS.TOAST.CANTSHOWINFO', true);

View File

@ -8,6 +8,8 @@
<input cnslInput formControlName="name" />
</cnsl-form-field>
<cnsl-info-section>{{ 'FLOWS.DIALOG.ABOUTNAME' | translate }}</cnsl-info-section>
<ngx-codemirror
*ngIf="opened$ | async"
formControlName="script"

View File

@ -4,7 +4,7 @@
<cnsl-refresh-table
[loading]="dataSource.loading$ | async"
*ngIf="projectId"
(refreshed)="loadGrantsPage()"
(refreshed)="refreshPage()"
[dataSize]="dataSource.totalResult"
[selection]="selection"
[timestamp]="dataSource.viewTimestamp"
@ -154,11 +154,11 @@
<cnsl-paginator
class="paginator"
#paginator
[pageSize]="50"
[pageSize]="INITIAL_PAGESIZE"
[timestamp]="dataSource.viewTimestamp"
[pageSizeOptions]="[25, 50, 100, 250]"
[length]="dataSource.totalResult"
(page)="loadGrantsPage($event.pageIndex, $event.pageSize)"
(page)="loadGrantsPage($event)"
>
</cnsl-paginator>
</cnsl-refresh-table>

View File

@ -6,7 +6,7 @@ import { MatSelectChange } from '@angular/material/select';
import { MatTable } from '@angular/material/table';
import { Router } from '@angular/router';
import { tap } from 'rxjs/operators';
import { PaginatorComponent } from 'src/app/modules/paginator/paginator.component';
import { PageEvent, PaginatorComponent } from 'src/app/modules/paginator/paginator.component';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { GrantedProject, ProjectGrantState, Role } from 'src/app/proto/generated/zitadel/project_pb';
import { ManagementService } from 'src/app/services/mgmt.service';
@ -14,8 +14,6 @@ import { ToastService } from 'src/app/services/toast.service';
import { ProjectGrantsDataSource } from './project-grants-datasource';
const ROUTEPARAM = 'projectid';
@Component({
selector: 'cnsl-project-grants',
templateUrl: './project-grants.component.html',
@ -28,7 +26,9 @@ const ROUTEPARAM = 'projectid';
]),
],
})
export class ProjectGrantsComponent implements OnInit, AfterViewInit {
export class ProjectGrantsComponent implements OnInit {
public INITIAL_PAGESIZE: number = 10;
@Input() public projectId: string = '';
@ViewChild(PaginatorComponent) public paginator!: PaginatorComponent;
@ViewChild(MatTable) public table!: MatTable<GrantedProject.AsObject>;
@ -51,16 +51,12 @@ export class ProjectGrantsComponent implements OnInit, AfterViewInit {
}
public ngOnInit(): void {
this.dataSource.loadGrants(this.projectId, 0, 25, 'asc');
this.dataSource.loadGrants(this.projectId, 0, this.INITIAL_PAGESIZE);
this.getRoleOptions(this.projectId);
}
public ngAfterViewInit(): void {
this.paginator.page.pipe(tap(() => this.loadGrantsPage())).subscribe();
}
public loadGrantsPage(pageIndex?: number, pageSize?: number): void {
this.dataSource.loadGrants(this.projectId, pageIndex ?? this.paginator.pageIndex, pageSize ?? this.paginator.pageSize);
public loadGrantsPage(event: PageEvent): void {
this.dataSource.loadGrants(this.projectId, event.pageIndex, event.pageSize);
}
public isAllSelected(): boolean {
@ -92,6 +88,11 @@ export class ProjectGrantsComponent implements OnInit, AfterViewInit {
});
}
public refreshPage(): void {
this.selection.clear();
this.dataSource.loadGrants(this.projectId, this.paginator.pageSize, this.paginator.pageIndex * this.paginator.pageSize);
}
public deleteGrant(grant: GrantedProject.AsObject): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {

View File

@ -12,6 +12,7 @@ import { InputModule } from 'src/app/modules/input/input.module';
import { DetailFormComponent } from './detail-form.component';
import { ProfilePictureComponent } from './profile-picture/profile-picture.component';
import { MatDialogModule } from '@angular/material/dialog';
@NgModule({
declarations: [DetailFormComponent, ProfilePictureComponent],
@ -26,6 +27,7 @@ import { ProfilePictureComponent } from './profile-picture/profile-picture.compo
MatButtonModule,
MatTooltipModule,
MatIconModule,
MatDialogModule,
TranslateModule,
InputModule,
],

View File

@ -97,12 +97,14 @@ import {
import { ChangeQuery } from '../proto/generated/zitadel/change_pb';
import { MetadataQuery } from '../proto/generated/zitadel/metadata_pb';
import { ListQuery } from '../proto/generated/zitadel/object_pb';
import { Org, OrgFieldName, OrgQuery } from '../proto/generated/zitadel/org_pb';
import { Org, OrgFieldName, OrgIDQuery, OrgQuery } from '../proto/generated/zitadel/org_pb';
import { LabelPolicy, PrivacyPolicy } from '../proto/generated/zitadel/policy_pb';
import { Gender, MembershipQuery, User, WebAuthNVerification } from '../proto/generated/zitadel/user_pb';
import { GrpcService } from './grpc.service';
import { StorageKey, StorageLocation, StorageService } from './storage.service';
const ORG_LIMIT = 10;
@Injectable({
providedIn: 'root',
})
@ -249,36 +251,49 @@ export class GrpcAuthService {
this.setActiveOrg(find);
return Promise.resolve(find);
} else {
const orgs = (await this.listMyProjectOrgs(10, 0)).resultList;
this.cachedOrgs.next(orgs);
const toFind = orgs.find((tmp) => tmp.id === id);
if (toFind) {
this.setActiveOrg(toFind);
return Promise.resolve(toFind);
const orgQuery = new OrgQuery();
const orgIdQuery = new OrgIDQuery();
orgIdQuery.setId(id);
orgQuery.setIdQuery(orgIdQuery);
const orgs = (await this.listMyProjectOrgs(ORG_LIMIT, 0, [orgQuery])).resultList;
if (orgs.length === 1) {
this.setActiveOrg(orgs[0]);
return Promise.resolve(orgs[0]);
} else {
return Promise.reject(new Error('requested organization not found'));
}
}
} else {
let orgs = this.cachedOrgs.getValue();
if (orgs.length === 0) {
orgs = (await this.listMyProjectOrgs()).resultList;
this.cachedOrgs.next(orgs);
}
const org = this.storage.getItem<Org.AsObject>(StorageKey.organization, StorageLocation.local);
if (org && orgs.find((tmp) => tmp.id === org.id)) {
this.storage.setItem(StorageKey.organization, org, StorageLocation.session);
this.setActiveOrg(org);
return Promise.resolve(org);
if (org) {
const orgQuery = new OrgQuery();
const orgIdQuery = new OrgIDQuery();
orgIdQuery.setId(org.id);
orgQuery.setIdQuery(orgIdQuery);
const specificOrg = (await this.listMyProjectOrgs(ORG_LIMIT, 0, [orgQuery])).resultList;
if (specificOrg.length === 1) {
this.setActiveOrg(specificOrg[0]);
return Promise.resolve(specificOrg[0]);
} else {
orgs = (await this.listMyProjectOrgs(ORG_LIMIT, 0)).resultList;
this.cachedOrgs.next(orgs);
}
} else {
orgs = (await this.listMyProjectOrgs(ORG_LIMIT, 0)).resultList;
this.cachedOrgs.next(orgs);
}
if (orgs.length === 0) {
this._activeOrgChanged.next(undefined);
return Promise.reject(new Error('No organizations found!'));
}
const orgToSet = orgs.find((element) => element.id !== '0' && element.name !== '');
const orgToSet = orgs.find((element) => element.id !== '0' && element.name !== '');
if (orgToSet) {
this.setActiveOrg(orgToSet);
return Promise.resolve(orgToSet);
@ -396,7 +411,7 @@ export class GrpcAuthService {
}
public async revalidateOrgs() {
const orgs = (await this.listMyProjectOrgs()).resultList;
const orgs = (await this.listMyProjectOrgs(ORG_LIMIT, 0)).resultList;
this.cachedOrgs.next(orgs);
}

View File

@ -29,7 +29,7 @@ export const OIDC_CONFIGURATIONS: OidcAppConfigurations = {
.setResponseTypesList([OIDCResponseType.OIDC_RESPONSE_TYPE_CODE])
.setGrantTypesList([OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE])
.setRedirectUrisList(['http://localhost:5173/auth/signinwin/zitadel'])
.setPostLogoutRedirectUrisList(['http://localhost:5173']),
.setPostLogoutRedirectUrisList(['http://localhost:5173/']),
// web applications
['next']: new AddOIDCAppRequest()
.setAppType(OIDCAppType.OIDC_APP_TYPE_WEB)

View File

@ -231,7 +231,7 @@
"LABEL": "Максимално времетраене в часове",
"PW_CHECK": {
"TITLE": "Проверка на парола",
"DESCRIPTION": "Вашите потребители трябва да сменят своите пароли в тези периоди."
"DESCRIPTION": "След този период от потребителите ще се изисква повторно удостоверяване с парола."
},
"EXT_LOGIN_CHECK": {
"TITLE": "Външна проверка за вход",
@ -1085,7 +1085,8 @@
"REMOVEACTIONSLIST": {
"TITLE": "Изтриване на избраните действия?",
"DESCRIPTION": "Сигурни ли сте, че искате да изтриете избраните действия от потока?"
}
},
"ABOUTNAME": "Името на действието и името на функцията в javascript трябва да са еднакви"
},
"TOAST": {
"ACTIONSSET": "Набор от действия",
@ -1436,9 +1437,7 @@
"7": "Еднократна парола (OTP) - SMS",
"8": "Еднократна парола (OTP) имейл"
},
"ADDGENERATOR": "Определете тайния външен вид",
"GENERATORTYPE": "Тип",
"EXPIRY": "Изтичане (в часове)",
"EXPIRY": "Изтичане (в минути)",
"INCLUDEDIGITS": "Включете числа",
"INCLUDESYMBOLS": "Включете символи",
"INCLUDELOWERLETTERS": "Включете малки букви",

View File

@ -231,7 +231,7 @@
"LABEL": "Maximální doba životnosti v hodinách",
"PW_CHECK": {
"TITLE": "Kontrola hesla",
"DESCRIPTION": "Tví uživatelé musí v těchto obdobích změnit svá hesla."
"DESCRIPTION": "Po uplynutí této doby budou uživatelé vyzváni k opětovné autentizaci pomocí hesla."
},
"EXT_LOGIN_CHECK": {
"TITLE": "Kontrola externího přihlášení",
@ -1086,7 +1086,8 @@
"REMOVEACTIONSLIST": {
"TITLE": "Smazat vybrané akce?",
"DESCRIPTION": "Jste si jisti, že chcete smazat vybrané akce z toku?"
}
},
"ABOUTNAME": "Název akce a název funkce v javascriptu musí být stejný"
},
"TOAST": {
"ACTIONSSET": "Akce nastaveny",
@ -1437,9 +1438,7 @@
"7": "Jednorázové heslo (OTP) - SMS",
"8": "Jednorázové heslo (OTP) - E-mail"
},
"ADDGENERATOR": "Definovat generátor tajemství",
"GENERATORTYPE": "Typ",
"EXPIRY": "Expirace (v hodinách)",
"EXPIRY": "Expirace (v minutách)",
"INCLUDEDIGITS": "Zahrnout čísla",
"INCLUDESYMBOLS": "Zahrnout symboly",
"INCLUDELOWERLETTERS": "Zahrnout malá písmena",

View File

@ -231,7 +231,7 @@
"LABEL": "Maximale Lebensdauer in Stunden",
"PW_CHECK": {
"TITLE": "Passwortüberprüfung",
"DESCRIPTION": "Deine Benutzer müssen ihre Passwörter in diesen Zeiträumen ändern."
"DESCRIPTION": "Nach diesem Zeitraum müssen sich Benutzer erneut mit ihrem Passwort authentifizieren."
},
"EXT_LOGIN_CHECK": {
"TITLE": "Externe Login-Überprüfung",
@ -1086,7 +1086,8 @@
"REMOVEACTIONSLIST": {
"TITLE": "Ausgewählte Aktionen löschen?",
"DESCRIPTION": "Wollen Sie die gewählten Aktionen wirklich löschen?"
}
},
"ABOUTNAME": "Der Name der Aktion und der Name der Funktion im Javascript müssen identisch sein"
},
"TOAST": {
"ACTIONSSET": "Aktionen gesetzt",
@ -1437,9 +1438,7 @@
"7": "One Time Password (OTP) - SMS",
"8": "One Time Password (OTP) - Email"
},
"ADDGENERATOR": "Passwort Generator definieren",
"GENERATORTYPE": "Typ",
"EXPIRY": "Ablauf (in Stunden)",
"EXPIRY": "Ablauf (in Minuten)",
"INCLUDEDIGITS": "Enthält Zahlen",
"INCLUDESYMBOLS": "Enthält Symbole",
"INCLUDELOWERLETTERS": "Enthält Kleinbuchstaben",

View File

@ -231,7 +231,7 @@
"LABEL": "Maximum Lifetime in hours",
"PW_CHECK": {
"TITLE": "Password Check",
"DESCRIPTION": "Your users have to change their passwords in these periods."
"DESCRIPTION": "Users will be required to re-authenticate with their password after this period."
},
"EXT_LOGIN_CHECK": {
"TITLE": "External Login Check",
@ -1086,7 +1086,8 @@
"REMOVEACTIONSLIST": {
"TITLE": "Delete selected Actions?",
"DESCRIPTION": "Are you sure you want to delete the selected actions from the flow?"
}
},
"ABOUTNAME": "The name of the action and the name of the function in the javascript have to be the same"
},
"TOAST": {
"ACTIONSSET": "Actions set",
@ -1437,9 +1438,7 @@
"7": "One Time Password (OTP) - SMS",
"8": "One Time Password (OTP) - Email"
},
"ADDGENERATOR": "Define Secret Generator",
"GENERATORTYPE": "Type",
"EXPIRY": "Expiration (in hours)",
"EXPIRY": "Expiration (in minutes)",
"INCLUDEDIGITS": "Include Numbers",
"INCLUDESYMBOLS": "Include Symbols",
"INCLUDELOWERLETTERS": "Include Lower letters",

View File

@ -231,7 +231,7 @@
"LABEL": "Duración Máxima en horas",
"PW_CHECK": {
"TITLE": "Verificación de Contraseña",
"DESCRIPTION": "Tus usuarios tienen que cambiar sus contraseñas en estos períodos."
"DESCRIPTION": "Los usuarios deberán volver a autenticarse con su contraseña después de este período."
},
"EXT_LOGIN_CHECK": {
"TITLE": "Verificación de Inicio de Sesión Externo",
@ -1086,7 +1086,8 @@
"REMOVEACTIONSLIST": {
"TITLE": "¿Borrar acciones seleccionadas?",
"DESCRIPTION": "¿Estás seguro que quieres borrar las acciones seleccionadas del flujo?"
}
},
"ABOUTNAME": "El nombre de la acción y el nombre de la función en javascript tienen que ser el mismo"
},
"TOAST": {
"ACTIONSSET": "Acciones establecidas",
@ -1438,9 +1439,7 @@
"7": "One Time Password (OTP) - SMS",
"8": "One Time Password (OTP) - email"
},
"ADDGENERATOR": "Configurar generador del secreto",
"GENERATORTYPE": "Tipo",
"EXPIRY": "Caducidad (en horas)",
"EXPIRY": "Caducidad (en minutos)",
"INCLUDEDIGITS": "Incluir números",
"INCLUDESYMBOLS": "Incluir símbolos",
"INCLUDELOWERLETTERS": "Incluir letras minúsculas",

View File

@ -231,7 +231,7 @@
"LABEL": "Durée de vie maximale en heures",
"PW_CHECK": {
"TITLE": "Vérification de Mot de Passe",
"DESCRIPTION": "Vos utilisateurs doivent changer leurs mots de passe pendant ces périodes."
"DESCRIPTION": "Les utilisateurs devront se réauthentifier avec leur mot de passe après cette période."
},
"EXT_LOGIN_CHECK": {
"TITLE": "Vérification de Connexion Externe",
@ -1086,7 +1086,8 @@
"REMOVEACTIONSLIST": {
"TITLE": "Supprimer les actions sélectionnées ?",
"DESCRIPTION": "Voulez-vous vraiment supprimer les actions sélectionnées du flux ?"
}
},
"ABOUTNAME": "Le nom de l'action et le nom de la fonction dans le javascript doivent être identiques"
},
"TOAST": {
"ACTIONSSET": "Actions définies",
@ -1437,9 +1438,7 @@
"7": "Mot de passe à usage unique (OTP) - SMS",
"8": "Mot de passe à usage unique (OTP) -e-mail"
},
"ADDGENERATOR": "Configurer générateur de mot de passe",
"GENERATORTYPE": "Type",
"EXPIRY": "Expiration (en heures)",
"EXPIRY": "Expiration (en minutes)",
"INCLUDEDIGITS": "Inclure les chiffres",
"INCLUDESYMBOLS": "Inclure les symboles",
"INCLUDELOWERLETTERS": "Inclure les lettres minuscules",

View File

@ -231,7 +231,7 @@
"LABEL": "Durata Massima in ore",
"PW_CHECK": {
"TITLE": "Controllo Password",
"DESCRIPTION": "I tuoi utenti devono cambiare le loro password in questi periodi."
"DESCRIPTION": "Gli utenti dovranno ri-autentificarsi con la password dopo questo periodo."
},
"EXT_LOGIN_CHECK": {
"TITLE": "Controllo Login Esterno",
@ -1085,7 +1085,8 @@
"REMOVEACTIONSLIST": {
"TITLE": "Elimina le azioni selezionate?",
"DESCRIPTION": "Sei sicuro di voler eliminare le azioni selezionate dal processo?"
}
},
"ABOUTNAME": "Il nome dell'azione e il nome della funzione nel javascript devono essere gli stessi"
},
"TOAST": {
"ACTIONSSET": "Azioni salvate!",
@ -1437,9 +1438,7 @@
"7": "One Time Password (OTP) - SMS",
"8": "One Time Password (OTP) - email"
},
"ADDGENERATOR": "Imposta il generatore di password",
"GENERATORTYPE": "Tipo",
"EXPIRY": "Scadenza (in ore)",
"EXPIRY": "Scadenza (in minuti)",
"INCLUDEDIGITS": "Contiene numeri",
"INCLUDESYMBOLS": "Contiene simboli",
"INCLUDELOWERLETTERS": "Contiene lettere minuscole",

View File

@ -231,7 +231,7 @@
"LABEL": "最大寿命(時間)",
"PW_CHECK": {
"TITLE": "パスワードチェック",
"DESCRIPTION": "ユーザーは、これらの期間中にパスワードを変更する必要があります。"
"DESCRIPTION": "この期間の後、ユーザーはパスワードで再認証する必要があります。"
},
"EXT_LOGIN_CHECK": {
"TITLE": "外部ログインチェック",
@ -1086,7 +1086,8 @@
"REMOVEACTIONSLIST": {
"TITLE": "選択したアクションの削除",
"DESCRIPTION": "フローから選択したアクションを削除してよろしいですか?"
}
},
"ABOUTNAME": "アクションの名前とJavaScript内の関数の名前は同じである必要があります"
},
"TOAST": {
"ACTIONSSET": "アクションセット",
@ -1437,9 +1438,7 @@
"7": "ワンタイムパスワード (OTP) - SMS",
"8": "ワンタイムパスワード (OTP) - 電子メール"
},
"ADDGENERATOR": "シークレットの設定を定義する",
"GENERATORTYPE": "タイプ",
"EXPIRY": "有効期限(時間単位)",
"EXPIRY": "有効期限 (分単位)",
"INCLUDEDIGITS": "数字を含める",
"INCLUDESYMBOLS": "シンボルを含める",
"INCLUDELOWERLETTERS": "小文字を含める",

View File

@ -231,7 +231,7 @@
"LABEL": "Максимално времетраење во часови",
"PW_CHECK": {
"TITLE": "Проверка на лозинка",
"DESCRIPTION": "Вашите корисници треба да ги променат своите лозинки во овие периоди."
"DESCRIPTION": "Потребителите ќе бидат потребни повторно да се аутентификуваат со својата лозинка по овој период."
},
"EXT_LOGIN_CHECK": {
"TITLE": "Проверка на надворешна најава",
@ -1086,7 +1086,8 @@
"REMOVEACTIONSLIST": {
"TITLE": "Избриши ги избраните акции?",
"DESCRIPTION": "Дали сте сигурни дека сакате да ги избришете избраните акции од Flow?"
}
},
"ABOUTNAME": "Името на дејството и името на функцијата во javascript треба да бидат исти"
},
"TOAST": {
"ACTIONSSET": "Акциите се поставени",
@ -1438,9 +1439,7 @@
"7": "Еднократна лозинка (OTP) - СМС",
"8": "Еднократна лозинка (OTP) - е-пошта"
},
"ADDGENERATOR": "Дефинирајте изглед на тајна",
"GENERATORTYPE": "Тип",
"EXPIRY": "Истекување (во часови)",
"EXPIRY": "Истекување (во минути)",
"INCLUDEDIGITS": "Вклучи цифри",
"INCLUDESYMBOLS": "Вклучи симболи",
"INCLUDELOWERLETTERS": "Вклучи мали букви",

View File

@ -231,7 +231,7 @@
"LABEL": "Maximale Levensduur in uren",
"PW_CHECK": {
"TITLE": "Wachtwoord Controle",
"DESCRIPTION": "Je gebruikers moeten hun wachtwoorden in deze perioden wijzigen."
"DESCRIPTION": "Gebruikers moeten na deze periode opnieuw worden geverifieerd met hun wachtwoord."
},
"EXT_LOGIN_CHECK": {
"TITLE": "Externe Login Controle",
@ -1086,7 +1086,8 @@
"REMOVEACTIONSLIST": {
"TITLE": "Geselecteerde acties verwijderen?",
"DESCRIPTION": "Weet u zeker dat u de geselecteerde acties uit de flow wilt verwijderen?"
}
},
"ABOUTNAME": "De naam van de actie en de naam van de functie in het javascript moeten hetzelfde zijn"
},
"TOAST": {
"ACTIONSSET": "Acties ingesteld",
@ -1437,9 +1438,7 @@
"7": "Eenmalig Wachtwoord (OTP) - SMS",
"8": "Eenmalig Wachtwoord (OTP) - Email"
},
"ADDGENERATOR": "Definieer Secret Generator",
"GENERATORTYPE": "Type",
"EXPIRY": "Vervaldatum (in uren)",
"EXPIRY": "Vervaldatum (in minuten)",
"INCLUDEDIGITS": "Inclusief Nummers",
"INCLUDESYMBOLS": "Inclusief Symbolen",
"INCLUDELOWERLETTERS": "Inclusief Kleinletters",

View File

@ -231,7 +231,7 @@
"LABEL": "Maksymalny czas życia w godzinach",
"PW_CHECK": {
"TITLE": "Sprawdzenie Hasła",
"DESCRIPTION": "Twoi użytkownicy muszą zmienić swoje hasła w tych okresach."
"DESCRIPTION": "Po tym okresie użytkownicy będą zobowiązani do ponownej autoryzacji za pomocą hasła."
},
"EXT_LOGIN_CHECK": {
"TITLE": "Sprawdzenie Logowania Zewnętrznego",
@ -1085,7 +1085,8 @@
"REMOVEACTIONSLIST": {
"TITLE": "Usunąć wybrane działanie?",
"DESCRIPTION": "Czy na pewno chcesz usunąć wybrane działania z przepływu?"
}
},
"ABOUTNAME": "Nazwa akcji i nazwa funkcji w JavaScript muszą być takie same"
},
"TOAST": {
"ACTIONSSET": "Ustawiono działanie",
@ -1436,9 +1437,7 @@
"7": "Hasło jednorazowe (OTP) - SMS",
"8": "Hasło jednorazowe (OTP) — e-mail"
},
"ADDGENERATOR": "Zdefiniuj wygląd sekretu",
"GENERATORTYPE": "Typ",
"EXPIRY": "Wygaśnięcie (w godzinach)",
"EXPIRY": "Wygaśnięcie (w minutach)",
"INCLUDEDIGITS": "Dołącz cyfry",
"INCLUDESYMBOLS": "Dołącz symbole",
"INCLUDELOWERLETTERS": "Dołącz małe litery",

View File

@ -231,7 +231,7 @@
"LABEL": "Duração Máxima em horas",
"PW_CHECK": {
"TITLE": "Verificação de Senha",
"DESCRIPTION": "Seus usuários devem mudar suas senhas nesses períodos."
"DESCRIPTION": "Os usuários serão obrigados a reautentificar-se com sua senha após este período."
},
"EXT_LOGIN_CHECK": {
"TITLE": "Verificação de Login Externo",
@ -1086,7 +1086,8 @@
"REMOVEACTIONSLIST": {
"TITLE": "Excluir Ações Selecionadas?",
"DESCRIPTION": "Tem certeza de que deseja excluir as ações selecionadas do fluxo?"
}
},
"ABOUTNAME": "O nome da ação e o nome da função no javascript devem ser iguais"
},
"TOAST": {
"ACTIONSSET": "Ações definidas",
@ -1438,9 +1439,7 @@
"7": "Senha única (OTP) - SMS",
"8": "Senha única (OTP) - e-mail"
},
"ADDGENERATOR": "Definir aparência de segredo",
"GENERATORTYPE": "Tipo",
"EXPIRY": "Expiração (em horas)",
"EXPIRY": "Expiração (em minutos)",
"INCLUDEDIGITS": "Incluir números",
"INCLUDESYMBOLS": "Incluir símbolos",
"INCLUDELOWERLETTERS": "Incluir letras minúsculas",

View File

@ -231,7 +231,7 @@
"LABEL": "Максимальный срок действия в часах",
"PW_CHECK": {
"TITLE": "Проверка пароля",
"DESCRIPTION": "Ваши пользователи должны менять свои пароли в эти периоды."
"DESCRIPTION": "После этого периода от пользователей потребуется повторная аутентификация с паролем."
},
"EXT_LOGIN_CHECK": {
"TITLE": "Проверка внешнего входа",
@ -1111,7 +1111,8 @@
"REMOVEACTIONSLIST": {
"TITLE": "Удалить выбранные действия?",
"DESCRIPTION": "Вы уверены, что хотите удалить выбранные действия из процесса?"
}
},
"ABOUTNAME": "Имя действия и имя функции в javascript должны быть одинаковыми."
},
"TOAST": {
"ACTIONSSET": "Набор действий",
@ -1489,9 +1490,7 @@
"7": "Одноразовый код (OTP) — SMS",
"8": "Одноразовый код (OTP)  электронная почта"
},
"ADDGENERATOR": "Определить отображение секретного ключа",
"GENERATORTYPE": "Тип",
"EXPIRY": "Срок действия (в часах)",
"EXPIRY": "Срок действия (в минутах)",
"INCLUDEDIGITS": "Содержит цифры",
"INCLUDESYMBOLS": "Содержит символы",
"INCLUDELOWERLETTERS": "Содержит строчные буквы",
@ -1869,7 +1868,6 @@
"GRANTED_SINGULAR": "Допуск проекта"
},
"PRIVATELABEL": {
"TITLE": "Настройка брендинга",
"0": {
"TITLE": "Не определено",
"DESC": "После идентификации пользователя, будет отображён брендинг организации идентифицированного пользователя, прежде чем будет показано системное значение по умолчанию."
@ -2441,7 +2439,7 @@
}
},
"NEXTSTEPS": {
"TITLE": "Дальнейшие шаги",
"TITLE": "Следующие шаги",
"0": {
"TITLE": "Добавить роли",
"DESC": "Введите свои роли в проекте"
@ -2453,8 +2451,7 @@
"2": {
"TITLE": "Помощь и поддержка",
"DESC": "Ознакомьтесь с нашей документацией по созданию приложений или обратитесь в нашу службу поддержки"
},
"TITLE": "Следующие шаги"
}
}
},
"NAMEDIALOG": {

View File

@ -231,7 +231,7 @@
"LABEL": "Maximal livslängd i timmar",
"PW_CHECK": {
"TITLE": "Lösenordskontroll",
"DESCRIPTION": "Dina användare måste ändra sina lösenord under dessa perioder."
"DESCRIPTION": "Användare kommer att behöva autentisera om med sitt lösenord efter denna period."
},
"EXT_LOGIN_CHECK": {
"TITLE": "Extern inloggningskontroll",
@ -1086,7 +1086,8 @@
"REMOVEACTIONSLIST": {
"TITLE": "Radera valda åtgärder?",
"DESCRIPTION": "Är du säker på att du vill radera de valda åtgärderna från flödet?"
}
},
"ABOUTNAME": "Namnet på åtgärden och namnet på funktionen i javascriptet måste vara detsamma"
},
"TOAST": {
"ACTIONSSET": "Åtgärder sparade",
@ -1441,9 +1442,7 @@
"7": "Engångslösenord (OTP) - SMS",
"8": "Engångslösenord (OTP) - E-post"
},
"ADDGENERATOR": "Definiera hemlighetsgenerator",
"GENERATORTYPE": "Typ",
"EXPIRY": "Utgång (i timmar)",
"EXPIRY": "Utgång (på minuter)",
"INCLUDEDIGITS": "Inkludera siffror",
"INCLUDESYMBOLS": "Inkludera symboler",
"INCLUDELOWERLETTERS": "Inkludera små bokstäver",

View File

@ -231,7 +231,7 @@
"LABEL": "最大有效期(小时)",
"PW_CHECK": {
"TITLE": "密码检查",
"DESCRIPTION": "您的用户必须在这些时期内更改他们的密码。"
"DESCRIPTION": "此期间后,用户将需要使用密码重新验证。"
},
"EXT_LOGIN_CHECK": {
"TITLE": "外部登录检查",
@ -1086,7 +1086,8 @@
"REMOVEACTIONSLIST": {
"TITLE": "删除选定的操作?",
"DESCRIPTION": "您确定要从流中删除选定的操作吗?"
}
},
"ABOUTNAME": "javascript 中的操作名称和函数名称必须相同"
},
"TOAST": {
"ACTIONSSET": "动作设置",
@ -1437,9 +1438,7 @@
"7": "一次性密码 (OTP) - SMS",
"8": "一次性密码 (OTP) - 电子邮件"
},
"ADDGENERATOR": "定义验证码外观",
"GENERATORTYPE": "类型",
"EXPIRY": "过期时间 (in hours)",
"EXPIRY": "过期时间 (以分钟为单位)",
"INCLUDEDIGITS": "包含数字",
"INCLUDESYMBOLS": "包含特殊符号",
"INCLUDELOWERLETTERS": "包含小写字母",

View File

@ -167,10 +167,10 @@ Adding metadata to users allows you to set default metadata on users.
#### Triggers
- Internal Authentication
- [Post Creation](/docs/apis/actions/internal-authentication#post-creation)
- [Pre Creation](/docs/apis/actions/internal-authentication#pre-creation)
- [Post Authentication](/docs/apis/actions/internal-authentication#post-authentication)
- External Authentication
- [Post Creation](/docs/apis/actions/internal-authentication#post-creation)
- [Pre Creation](/docs/apis/actions/internal-authentication#pre-creation)
- [Post Authentication](/docs/apis/actions/internal-authentication#post-authentication)
<details open="">

View File

@ -136,21 +136,25 @@ Additional to the standard CRUD methods:
- ListAvailableExecutionMethods
- ListAvailableExecutionFunctions
\<details><summary>action_service.proto</summary>
<CodeBlock language="protobuf">{ActionServiceProto}</CodeBlock>
\</details>
<details>
<summary>action_service.proto</summary>
<CodeBlock language="protobuf">{ActionServiceProto}</CodeBlock>
</details>
\<details><summary>action_target.proto</summary>
<CodeBlock language="protobuf">{ActionTargetProto}</CodeBlock>
\</details>
<details>
<summary>action_target.proto</summary>
<CodeBlock language="protobuf">{ActionTargetProto}</CodeBlock>
</details>
\<details><summary>action_execution.proto</summary>
<CodeBlock language="protobuf">{ActionExecutionProto}</CodeBlock>
\</details>
<details>
<summary>action_execution.proto</summary>
<CodeBlock language="protobuf">{ActionExecutionProto}</CodeBlock>
</details>
\<details><summary>action_query.proto</summary>
<CodeBlock language="protobuf">{ActionSearchProto}</CodeBlock>
\</details>
<details>
<summary>action_query.proto</summary>
<CodeBlock language="protobuf">{ActionSearchProto}</CodeBlock>
</details>
### ZITADELUsers
@ -165,29 +169,35 @@ Standard CRUD methods
- Standard CRUD and methods for all IDPs
- Resources have additional properties for reusability capabilities.
\<details><summary>idp_service.proto</summary>
<CodeBlock language="protobuf">{IDPServiceProto}</CodeBlock>
\</details>
<details>
<summary>idp_service.proto</summary>
<CodeBlock language="protobuf">{IDPServiceProto}</CodeBlock>
</details>
\<details><summary>idp.proto</summary>
<CodeBlock language="protobuf">{IDPProto}</CodeBlock>
\</details>
<details>
<summary>idp.proto</summary>
<CodeBlock language="protobuf">{IDPProto}</CodeBlock>
</details>
\<details><summary>idp_search.proto</summary>
<CodeBlock language="protobuf">{IDPSearchProto}</CodeBlock>
\</details>
<details>
<summary>idp_search.proto</summary>
<CodeBlock language="protobuf">{IDPSearchProto}</CodeBlock>
</details>
\<details><summary>idp_gitlab.proto</summary>
<CodeBlock language="protobuf">{IDPGitLabProto}</CodeBlock>
\</details>
<details>
<summary>idp_gitlab.proto</summary>
<CodeBlock language="protobuf">{IDPGitLabProto}</CodeBlock>
</details>
\<details><summary>object.proto</summary>
<CodeBlock language="protobuf">{ObjectProto}</CodeBlock>
\</details>
<details>
<summary>object.proto</summary>
<CodeBlock language="protobuf">{ObjectProto}</CodeBlock>
</details>
\<details><summary>resource_object.proto</summary>
<CodeBlock language="protobuf">{ResourceObjectProto}</CodeBlock>
\</details>
<details>
<summary>resource_object.proto</summary>
<CodeBlock language="protobuf">{ResourceObjectProto}</CodeBlock>
</details>
### ZITADELInstances
@ -267,21 +277,25 @@ For a full proto example, have a look at the [ZITADELLanguageSettings service](#
Default language, restricted languages, supported languages
\<details><summary>language_service.proto</summary>
<CodeBlock language="protobuf">{LanguageServiceProto}</CodeBlock>
\</details>
<details>
<summary>language_service.proto</summary>
<CodeBlock language="protobuf">{LanguageServiceProto}</CodeBlock>
</details>
\<details><summary>language.proto</summary>
<CodeBlock language="protobuf">{LanguageProto}</CodeBlock>
\</details>
<details>
<summary>language.proto</summary>
<CodeBlock language="protobuf">{LanguageProto}</CodeBlock>
</details>
\<details><summary>object.proto</summary>
<CodeBlock language="protobuf">{ObjectProto}</CodeBlock>
\</details>
<details>
<summary>object.proto</summary>
<CodeBlock language="protobuf">{ObjectProto}</CodeBlock>
</details>
\<details><summary>settings_object.proto</summary>
<CodeBlock language="protobuf">{SettingsObjectProto}</CodeBlock>
\</details>
<details>
<summary>settings_object.proto</summary>
<CodeBlock language="protobuf">{SettingsObjectProto}</CodeBlock>
</details>
### ZITADELTextSettings

View File

@ -1,6 +1,6 @@
---
title: Configure LinkedIn as an OAuth Identity Provider in ZITADEL
sidebar_label: LinkedIn generic OIDC
sidebar_label: LinkedIn generic OAuth
id: linkedin-oauth
---

View File

@ -0,0 +1,285 @@
---
title: Authenticate users with SAML
sidebar_label: SAML
---
SAML stands for Security Assertion Markup Language. It is a standard commonly used for identity federation and single sign-on (SSO). It is one of the original and most popular standards for SSO. Although it is prone to certain security flaws and exploits if not implemented correctly, it remains relevant and widely used.
## Why use SAML?
Here are some reasons why organizations might choose SAML:
**Legacy systems compatibility**
SAML has been in use since 2002 and is deeply integrated into many legacy systems and enterprise environments. Organizations with existing SAML infrastructure may prefer to continue using it to avoid costly and complex migrations.
**Enterprise use cases**
SAML is often favored in enterprise settings where detailed user attributes and complex authorization requirements are necessary. Its support for rich metadata and customizable assertions makes it suitable for intricate access control scenarios.
**Mature ecosystem**
The SAML ecosystem is mature, with extensive support from a wide range of enterprise applications and identity providers. This broad compatibility ensures that SAML can be used seamlessly across various platforms and services.
## Common SAML terms
- **Service Provider (SP)**: The application the user is trying to sign into.
- **Identity Provider (IdP)**: The centralized point of authentication.
- **SAML Request**: A communication from the SP to the IdP.
- **SAML Response**: A communication from the IdP to the SP, containing assertions about the user.
- **Assertions**: Statements within the SAML response about the user, signed using XML signatures.
- **Assertion Consumer Service (ACS)**: The endpoint at the SP responsible for processing the SAML response.
- **Attributes**: User information within the SAML response.
- **Relay State**: A way for the IdP to remember where a user was before authentication.
- **SAML Trust**: The configuration between the identity provider and the service provider
- **Metadata**: Trust information exchanged between the identity provider and service provider
## SAML explained
The **Service Provider (SP)** is the application that the user is trying to sign into. When the service provider sends a communication to the identity provider, it is called a **SAML request**. When the **Identity Provider (IdP)** responds or sends a communication to the service provider, it is called a SAML response.
Within the SAML response, there are multiple statements about the user, known as **assertions**. These assertions are all signed using XML signatures (also known as DSig) and are sent to be processed at the service provider's specific endpoint called an **Assertion Consumer Service (ACS)**. The ACS is responsible for receiving the SAML response from the identity provider, checking the assertions' signatures, and validating the entire document. This is a crucial part of implementing SAML from the service provider's perspective.
Additionally, the SAML response contains other pieces of information about the user, such as their first name, last name, and other profile information, referred to as **attributes**.
Another important concept in SAML is the **relay state**. The relay state allows the identity provider to remember where a user was before authentication. If a user is browsing anonymously through the service provider and triggers authentication, they will be redirected to the identity provider. After validating the user's identity, the identity provider will redirect them back to the service provider's ACS. The relay state makes sure that the user returns to their original location instead of being dropped on a generic home page.
**SAML trust** is the configuration between the identity provider and the service provider, involving shared information such as a signing certificate and an entity ID (also known as an issuer) from the identity provider. This shared information establishes a trust that allows both parties to validate communications, requests, and responses.
**Metadata** is another crucial term in SAML. Metadata allows for self-configuration between the identity provider and the service provider. Instead of manually exchanging certificates, endpoint URLs, and issuer information, metadata enables the sharing of an XML configuration file or URLs to these files. This allows the service provider and identity provider to self-configure based on the information within these configuration files, making the process less manual and more convenient.
## SAML workflow
One important aspect of SAML is that the user can initiate the authentication process in two primary workflows:
**1. IdP-Initiated: The user starts at the IdP, which sends a SAML response to the SP.**
Commonly, with workforce identity providers, there is a centralized user portal where a user can see a list of applications. Clicking on one of these applications initiates the authentication process without the user first going to the service provider. This method is called IdP-initiated (Identity Provider-initiated) authentication. In this flow, the user goes to the identity provider, which automatically kicks off the SAML response, directing them to the desired application.
**2. SP-Initiated: The user starts at the SP, which sends a SAML request to the IdP, followed by a SAML response from the IdP.**
The other method is SP-initiated (Service Provider-initiated) authentication. Here, the user starts at the service provider, which then sends a SAML request to the identity provider. The identity provider processes this request and sends back a SAML response.
In IdP-initiated flows, there is no SAML request, while in SP-initiated flows, the SAML request and response are both involved. The identity provider must understand how to receive a SAML request and create a SAML response. The service provider, particularly its Assertion Consumer Service (ACS), needs to validate the SAML response and potentially generate a SAML request if SP-initiated flow is supported.
It's important to note that not all service providers support both methods. Some only support IdP-initiated, while others only support SP-initiated. The choice between supporting IdP-initiated or SP-initiated authentication depends on the specific requirements of the product and the preferences of the developer implementing SAML. ZITADEL, for instance, supports only the SP-initiated flow.
## SAML requests and responses
SAML uses XML for both requests and responses. A typical SAML request from an SP to an IdP includes an ID, timestamp, destination URL, and issuer information. The IdP processes this request and returns a SAML response containing user assertions, which the SP validates.
Let's delve into what a SAML request and response look like with the following shortened examples.
**Sample SAML request**
```xml
<?xml version="1.0" encoding="utf-8"?>
<ns0:AuthnRequest
xmlns:ns0="urn:oasis:names:tc:SAML:2.0:protocol" ID="id-8LjuzBEUQFYFWjL55" Version="2.0" IssueInstant="2024-06-11T04:13:52Z" Destination="https://my-instance-xtzfbc.zitadel.cloud/saml/v2/SSO" AssertionConsumerServiceURL="http://127.0.0.1:5000/acs">
<ns1:Issuer
xmlns:ns1="urn:oasis:names:tc:SAML:2.0:assertion" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">https://zitadel-test.sp/metadata
</ns1:Issuer>
<ns2:Signature
xmlns:ns2="http://www.w3.org/2000/09/xmldsig#">
<ns2:SignedInfo>
<ns2:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ns2:CanonicalizationMethod>
<ns2:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"></ns2:SignatureMethod>
<ns2:Reference URI="#id-8LjuzBEUQFYFWjL55">
<ns2:Transforms>
<ns2:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></ns2:Transform>
<ns2:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ns2:Transform>
</ns2:Transforms>
<ns2:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></ns2:DigestMethod>
<ns2:DigestValue>WSz/EQ72RZ0DTh3DBSRCElpITqM=</ns2:DigestValue>
</ns2:Reference>
</ns2:SignedInfo>
<ns2:SignatureValue>HHjsNh0OLj7...</ns2:SignatureValue>
<ns2:KeyInfo>
<ns2:X509Data>
<ns2:X509Certificate>MIIDtzCCAp+gAwIBAgIUfITRQGue...</ns2:X509Certificate>
</ns2:X509Data>
</ns2:KeyInfo>
</ns2:Signature>
</ns0:AuthnRequest>
```
In this SAML request:
- The **ID** (id-8LjuzBEUQFYFWjL55) is generated by the service provider, and the identity provider will respond to this ID in the SAML response.
- **IssueInstant** (2024-05-11T04:13:52Z) is the timestamp for this request, used by the identity provider for verification to ensure the request is within an acceptable time frame.
- **Destination** (https://my-instance-xtzfbc.zitadel.cloud/saml/v2/SSO) points to the identity provider's URL, ensuring the request is sent to the correct recipient.
- **AssertionConsumerServiceURL** (http://127.0.0.1:5000/acs) indicates where the response should be sent after user authentication.
- **Issuer** (https://zitadel-test.sp/metadata) is a predefined string formatted as a URL, matching what the identity provider expects.
- **Signature** ensures the integrity and authenticity of the SAML request. It includes:
- **SignedInfo** contains details about the canonicalization and signature methods.
- **Reference** points to the signed data and includes a transform and digest method.
- **SignatureValue** provides the actual digital signature.
- **KeyInfo** containing the X.509 certificate, which holds the public key used to verify the signature.
These XMLs are stringified, encoded, and transmitted according to the SAML binding used. For instance, in HTTP Redirect Binding, the request is sent as URL parameters, while in HTTP POST Binding, it is included in the request body. The signature's transmission method also depends on the binding used, ensuring the integrity and authenticity of the SAML message across different communication channels.
**Sample SAML response**
```xml
<?xml version="1.0" encoding="utf-8"?>
<Response
xmlns="urn:oasis:names:tc:SAML:2.0:protocol" ID="_164ba12b-6711-40e0-8ddb-55aa810f1c92" InResponseTo="id-8LjuzBEUQFYFWjL55" Version="2.0" IssueInstant="2024-06-11T04:17:41Z" Destination="http://127.0.0.1:5000/acs">
<Issuer
xmlns="urn:oasis:names:tc:SAML:2.0:assertion" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">https://my-instance-xtzfbc.zitadel.cloud/saml/v2/metadata
</Issuer>
<Status>
<StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"></StatusCode>
</Status>
<Assertion
xmlns="urn:oasis:names:tc:SAML:2.0:assertion" Version="2.0" ID="_6fbdb616-b77f-46af-9554-989c8b89eeda" IssueInstant="2024-06-11T04:17:41Z">
<Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">https://my-instance-xtzfbc.zitadel.cloud/saml/v2/metadata</Issuer>
<Signature
xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></CanonicalizationMethod>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"></SignatureMethod>
<Reference URI="#_6fbdb616-b77f-46af-9554-989c8b89eeda">
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></Transform>
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></Transform>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"></DigestMethod>
<DigestValue>/b1R9LJJSeNX...</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>n5GbV4xhkXV...</SignatureValue>
<KeyInfo>
<X509Data>
<X509Certificate>MIIFITCCAwmgAwIBAgIBUTANBgkqh...</X509Certificate>
</X509Data>
</KeyInfo>
</Signature>
<Subject>
<NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">dakshitha.devrel@gmail.com</NameID>
<SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<SubjectConfirmationData NotOnOrAfter="2024-06-11T04:22:41Z" Recipient="http://127.0.0.1:5000/acs" InResponseTo="id-8LjuzBEUQFYFWjL55"></SubjectConfirmationData>
</SubjectConfirmation>
</Subject>
<Conditions NotBefore="2024-06-11T04:17:41Z" NotOnOrAfter="2024-06-11T04:22:41Z">
<AudienceRestriction>
<Audience>https://zitadel-test.sp/metadata</Audience>
</AudienceRestriction>
</Conditions>
<AuthnStatement AuthnInstant="2024-06-11T04:17:41Z" SessionIndex="_6fbdb616-b77f-46af-9554-989c8b89eeda">
<AuthnContext>
<AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</AuthnContextClassRef>
</AuthnContext>
</AuthnStatement>
<AttributeStatement>
<Attribute Name="Email" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<AttributeValue>tony.stark@gmail.com</AttributeValue>
</Attribute>
<Attribute Name="SurName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<AttributeValue>Stark</AttributeValue>
</Attribute>
<Attribute Name="FirstName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<AttributeValue>Tony</AttributeValue>
</Attribute>
<Attribute Name="FullName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<AttributeValue>Tony Stark</AttributeValue>
</Attribute>
<Attribute Name="UserName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<AttributeValue>tony.stark@gmail.com</AttributeValue>
</Attribute>
<Attribute Name="UserID" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<AttributeValue>260242264868201995</AttributeValue>
</Attribute>
</AttributeStatement>
</Assertion>
</Response>
```
In this SAML response:
- The **ID** (_164ba12b-6711-40e0-8ddb-55aa810f1c92) matches the ID from the SAML request (id-8LjuzBEUQFYFWjL55), allowing the service provider to verify it.
- **InResponseTo** references the ID of the SAML request.
- **IssueInstant** (2024-06-11T04:17:41Z) is the timestamp of the response.
- **Destination** (http://127.0.0.1:5000/acs) confirms that the response is intended for the service provider's Assertion Consumer Service URL.
- **Issuer** (https://my-instance-xtzfbc.zitadel.cloud/saml/v2/metadata) is the identity provider's unique string, used to verify the response's origin.
- **StatusCode** indicates the status of the authentication process, with a value of "Success." If the status is not "Success," it means there was a problem with the login, such as incorrect credentials or other authentication issues.
- **Signature** ensures the integrity and authenticity of the SAML response. It includes:
- **SignedInfo** contains details about the canonicalization and signature methods.
- **Reference** points to the signed data and including transforms and a digest method.
- **SignatureValue** provides the actual digital signature.
- **KeyInfo** contains the X.509 certificate, which holds the public key used to verify the signature.
- **Assertion** includes user information, such as email, surname, first name, full name, username, user ID etc.
- **Subject** contains the user's unique identifier.
- **Conditions** specify the response's validity window.
- **AuthnStatement** includes authentication details.
- **AttributeStatement** contains additional user attributes.
## SAML identity brokering
### How SAML identity brokering works
- **Initial Authentication Request**: A user attempts to access a service (SP1) that is protected by an IdP (IdP1).
- **Redirection to IdP1**: The user is redirected to IdP1 for authentication. If IdP1 trusts another IdP (IdP2) for authentication, it will redirect the user to IdP2.
- **Authentication at IdP2**: The user authenticates with IdP2, which generates a SAML assertion containing the user's identity and attributes.
- **Assertion Processing**: ZITADEL (IdP1) processes the SAML assertion from IdP2. ZITADEL then creates an independent SAML assertion based on the information received and its own policies.
- **Response to SP1**: ZITADEL (IdP1) sends this newly created SAML assertion to SP1, completing the authentication process.
- **Access Granted to SP1**:IdP1 then sends a final SAML assertion to SP1, which grants the user access to the requested service.
See [Let Users Login with Preferred Identity Provider](https://zitadel.com/docs/guides/integrate/identity-providers/introduction) for more information.
## Best practices for SAML implementation
Implementing SAML securely involves several best practices:
- Limit XML Parser Features: Disable unnecessary features to prevent XML external entity (XXE) attacks.
- Use Canonicalized XML: Normalize XML to prevent manipulation.
- Validate XML Schema: Ensure only expected XML formats are accepted.
- Validate Signatures: Check all signatures in the SAML response.
- Use SSL Encryption: Protect against interception.
- Validate Parties: Ensure the destination, audience, recipient, and issuer information is correct.
- Enforce Validation Window: Accept responses only within a valid time frame.
- Use Historical Cache: Track and reject duplicate IDs to prevent replay attacks.
- Minimize Buffer Size: Protect against DDoS attacks.
## Alternatives to SAML
SAML has its flaws; it can be complex and cumbersome to implement. The primary alternatives to consider is OpenID Connect (OIDC). Some illustrate this with the following analogy: “SAML is to OpenID Connect as SOAP is to REST." Just as REST was created to address some inherent flaws in SOAP, OpenID Connect was created to address some of the limitations in specifications like SAML. OpenID Connect is flexible, easy to use, widely adopted, and reliably secure.
For example, SAML is not well-suited for desktop and mobile applications due to its reliance on HTTP redirects, cookie-based session management, and complex certificate handling. OIDC, on the other hand, offers a modern, flexible, and simpler solution for authentication and authorization needs across desktop and mobile applications. It uses authorization codes and tokens that are easier to manage and supports custom URL schemes and deep linking, which simplifies the handling of redirects and improves the overall user experience. Furthermore, OIDC offers more flexibility in managing sessions without relying solely on cookies.
If a project requires SAML due to specific requirements or existing infrastructure, it should be used. However, for new projects, it is advisable to consider OpenID Connect because it is a more modern standard and is the more popular choice in the industry.
## Testing SAML scenarios using ZITADEL
To test SAML scenarios with ZITADEL, follow these steps:
1. Integrate a SAML SP with ZITADEL as the IdP:
- Sign up for a ZITADEL account if you don't already have one. If you are self-hosting ZITADEL, you can skip this step.
- Create an Organization and a Project in ZITADEL.
- Within your project, create a SAML application.
- Follow this example on how to create a SAML SP and integrate ZITADEL as the SAML IdP: [ZITADEL Python SAML SP Integration](https://github.com/zitadel/python-saml-sp).
2. Integrate ZITADEL with another SAML IdP for identity brokering:
- Configure an identity provider that supports SAML.
- Set up the necessary metadata and endpoints. Here are some guides to help with this setup:
- [Configure Entra ID as a SAML IdP](https://zitadel.com/docs/guides/integrate/identity-providers/azure-ad-saml)
- [Configure Okta as a SAML IdP](https://zitadel.com/docs/guides/integrate/identity-providers/okta-saml)
- [Configure MockSAML as a SAML IdP](https://zitadel.com/docs/guides/integrate/identity-providers/mocksaml)
3. Create test users and simulate authentication requests:
- Create test users in ZITADEL.
- Simulate authentication requests to verify that the SAML assertions are correctly generated and transmitted.
- Ensure that the SAML assertions contain the expected attributes and that these attributes are correctly processed by the service provider.
4. Simulate various scenarios:
- Successful Login: Verify that a valid user can successfully authenticate and access the service.
- Failed Login: Test scenarios where authentication fails, such as incorrect credentials or disabled accounts.
- Attribute Mapping: Check that user attributes (e.g., roles, permissions) are correctly mapped and utilized by the service provider.
- Logout Requests: Test single logout (SLO) to ensure that logging out from one service logs the user out of all connected services.
For more information, refer to [SAML Endpoints in ZITADEL](https://zitadel.com/docs/apis/saml/endpoints).

View File

@ -49,6 +49,9 @@ curl --request POST \
If you want to access ZITADEL APIs, make sure to include the required scopes `urn:zitadel:iam:org:project:id:zitadel:aud`.
Read our guide [how to access ZITADEL APIs](../zitadel-apis/access-zitadel-apis) to learn more.
**Important Note:** If the service user token needs to be validated using token introspection, ensure you include the `urn:zitadel:iam:org:project:id:{projectid}:aud` scope in your token request.
Without this, token introspection will fail.
You should receive a successful response with `access_token`, `token_type` and time to expiry in seconds as `expires_in`.
```bash

View File

@ -163,6 +163,9 @@ curl --request POST \
If you want to access ZITADEL APIs, make sure to include the required scopes `urn:zitadel:iam:org:project:id:zitadel:aud`.
Read our guide [how to access ZITADEL APIs](../zitadel-apis/access-zitadel-apis) to learn more.
**Important Note:** If the service user token needs to be validated using token introspection, ensure you include the `urn:zitadel:iam:org:project:id:{projectid}:aud` scope in your token request.
Without this, token introspection will fail.
You should receive a successful response with `access_token`, `token_type` and time to expiry in seconds as `expires_in`.
```bash

View File

@ -129,11 +129,10 @@ The snippets in the sections below are parts from the bulk import endpoint, to c
### Passwords
Passwords are stored only as hash.
You can transfer the hashes as long as ZITADEL [supports the same hash algorithm](/docs/concepts/architecture/secrets#hashed-secrets).
Password change on the next sign-in can be enforced.
ZITADEL stores passwords only as irreversible hashes, never in clear text.
Existing password hashes can be imported if they use a supported [hash algorithm](/docs/concepts/architecture/secrets#hashed-secrets).
_snippet from [bulk-import](#bulk-import) example:_
Import password hashes using the import API (snippet from [bulk-import](#bulk-import)):
```json
{
"userName": "test9@test9",
@ -147,13 +146,32 @@ _snippet from [bulk-import](#bulk-import) example:_
}
```
Upon initial login, ZITADEL validates the imported password using the appropriate verifier.
:::info Verifiers
In ZITADEL, a password verifier checks the validity of a password hash created with an algorithm different from the currently configured one.
It acts as a translator, allowing ZITADEL to understand and validate hashes made with older algorithms like MD5 even when the system has transitioned to newer ones like Argon2.
This is crucial during migrations or when importing user data.
Essentially, a verifier ensures ZITADEL can work with passwords hashed using various algorithms, maintaining security while transitioning to stronger hashing methods.
:::
Regardless of the `passwordChangeRequired` setting, the password is rehashed using the configured hasher algorithm and stored.
This ensures consistency and allows for automatic updates even when hasher configurations are changed, such as increasing salt cost for bcrypt.
To configure the default hasher for new user passwords, set the `Algorithm` of the `PasswordHasher` in the [runtime configuration file](/docs/self-hosting/manage/configure#runtime-configuration-file)
or by the environment variable `ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_ALGORITHM`, for example:
```
ZITADEL_SYSTEMDEFAULTS_PASSWORDHASHER_HASHER_ALGORITHM='pbkdf2'
```
Hasher configuration updates will automatically rehash existing passwords when they are validated or changed.
In case the hashes can't be transferred directly, you always have the option to create a user in ZITADEL without password and prompt users to create a new password.
If your legacy system receives the passwords in clear text (eg, login form) you could also directly create users via ZITADEL API. We will explain this pattern in more detail in this guide.
:::info
In case the hash algorithm you are using is not supported by ZITADEL, please let us know after searching our discussions, issues, and chat for similar requests.
:::
If your legacy system receives the passwords in clear text (eg, login form) you could also directly create users via ZITADEL API.
We will explain this pattern in more detail in this guide.
### One-time-passwords (OTP)

View File

@ -18,9 +18,10 @@ The setup is tested against Docker version 20.10.17 and Docker Compose version v
By executing the commands below, you will download the following file:
\<details><summary>docker-compose.yaml</summary>
<details>
<summary>docker-compose.yaml</summary>
<CodeBlock language="yaml">{DockerComposeSource}</CodeBlock>
\</details>
</details>
```bash
# Download the docker compose example configuration.
@ -41,9 +42,10 @@ docker compose up --detach
By executing the commands below, you will download the following file:
\<details><summary>docker-compose-sa.yaml</summary>
<details>
<summary>docker-compose-sa.yaml</summary>
<CodeBlock language="yaml">{DockerComposeSaSource}</CodeBlock>
\</details>
</details>
```bash
# Download the docker compose example configuration.

View File

@ -21,21 +21,26 @@ The setup is tested against Docker version 20.10.17 and Docker Compose version v
By executing the commands below, you will download the following files:
\<details><summary>docker-compose.yaml</summary>
<details>
<summary>docker-compose.yaml</summary>
<CodeBlock language="yaml">{DockerComposeSource}</CodeBlock>
\</details>
\<details><summary>example-traefik.yaml</summary>
</details>
<details>
<summary>example-traefik.yaml</summary>
<CodeBlock language="yaml">{ExampleTraefikSource}</CodeBlock>
\</details>
\<details><summary>example-zitadel-config.yaml</summary>
</details>
<details>
<summary>example-zitadel-config.yaml</summary>
<CodeBlock language="yaml">{ExampleZITADELConfigSource}</CodeBlock>
\</details>
\<details><summary>example-zitadel-secrets.yaml</summary>
</details>
<details>
<summary>example-zitadel-secrets.yaml</summary>
<CodeBlock language="yaml">{ExampleZITADELSecretsSource}</CodeBlock>
\</details>
\<details><summary>example-zitadel-init-steps.yaml</summary>
</details>
<details>
<summary>example-zitadel-init-steps.yaml</summary>
<CodeBlock language="yaml">{ExampleZITADELInitStepsSource}</CodeBlock>
\</details>
</details>
```bash
# Download the docker compose example configuration.

View File

@ -8,18 +8,25 @@ The docker compose example mounts the example zitadel configuration files to the
By executing the commands below, you will download the following files:
\<details><summary>docker-compose.yaml</summary>
<details>
<summary>docker-compose.yaml</summary>
<CodeBlock language="yaml">{DockerComposeSource}</CodeBlock>
\</details>
\<details><summary>example-zitadel-config.yaml</summary>
</details>
<details>
<summary>example-zitadel-config.yaml</summary>
<CodeBlock language="yaml">{ExampleZITADELConfigSource}</CodeBlock>
\</details>
\<details><summary>example-zitadel-secrets.yaml</summary>
</details>
<details>
<summary>example-zitadel-secrets.yaml</summary>
<CodeBlock language="yaml">{ExampleZITADELSecretsSource}</CodeBlock>
\</details>
\<details><summary>example-zitadel-init-steps.yaml</summary>
</details>
<details>
<summary>example-zitadel-init-steps.yaml</summary>
<CodeBlock language="yaml">{ExampleZITADELInitStepsSource}</CodeBlock>
\</details>
</details>
```bash
# Download the docker compose example configuration.

View File

@ -9,12 +9,15 @@ For a secure installation with Docker Compose, [go to the loadbalancing example]
By executing the commands below, you will download the following files:
\<details><summary>example-zitadel-values.yaml</summary>
<details>
<summary>example-zitadel-values.yaml</summary>
<CodeBlock language="yaml">{ExampleZITADELValuesSource}</CodeBlock>
\</details>
\<details><summary>example-zitadel-values-secrets.yaml</summary>
</details>
<details>
<summary>example-zitadel-values-secrets.yaml</summary>
<CodeBlock language="yaml">{ExampleZITADELValuesSecretsSource}</CodeBlock>
\</details>
</details>
```bash
# Download and adjust the example configuration file containing standard configuration

View File

@ -7,15 +7,20 @@ import ExampleZITADELInitStepsSource from '!!raw-loader!./example-zitadel-init-s
By executing the commands below, you will download the following files:
\<details><summary>example-zitadel-config.yaml</summary>
<details>
<summary>example-zitadel-config.yaml</summary>
<CodeBlock language="yaml">{ExampleZITADELConfigSource}</CodeBlock>
\</details>
\<details><summary>example-zitadel-secrets.yaml</summary>
</details>
<details>
<summary>example-zitadel-secrets.yaml</summary>
<CodeBlock language="yaml">{ExampleZITADELSecretsSource}</CodeBlock>
\</details>
\<details><summary>example-zitadel-init-steps.yaml</summary>
</details>
<details>
<summary>example-zitadel-init-steps.yaml</summary>
<CodeBlock language="yaml">{ExampleZITADELInitStepsSource}</CodeBlock>
\</details>
</details>
```bash
# Download and adjust the example configuration file containing standard configuration

View File

@ -194,6 +194,12 @@ module.exports = {
disableSwitch: false,
respectPrefersColorScheme: true,
},
codeblock: {
showGithubLink: true,
githubLinkLabel: 'View on GitHub',
showRunmeLink: false,
runmeLinkLabel: 'Checkout via Runme'
},
},
webpack: {
jsLoader: (isServer) => ({
@ -283,7 +289,7 @@ module.exports = {
outputDir: "docs/apis/resources/user_service",
sidebarOptions: {
groupPathsBy: "tag",
categoryLinkSource: "tag",
categoryLinkSource: "auto",
},
},
session: {
@ -291,7 +297,7 @@ module.exports = {
outputDir: "docs/apis/resources/session_service",
sidebarOptions: {
groupPathsBy: "tag",
categoryLinkSource: "tag",
categoryLinkSource: "auto",
},
},
oidc: {
@ -299,7 +305,7 @@ module.exports = {
outputDir: "docs/apis/resources/oidc_service",
sidebarOptions: {
groupPathsBy: "tag",
categoryLinkSource: "tag",
categoryLinkSource: "auto",
},
},
settings: {
@ -307,7 +313,7 @@ module.exports = {
outputDir: "docs/apis/resources/settings_service",
sidebarOptions: {
groupPathsBy: "tag",
categoryLinkSource: "tag",
categoryLinkSource: "auto",
},
},
user_schema: {
@ -315,7 +321,7 @@ module.exports = {
outputDir: "docs/apis/resources/user_schema_service_v3",
sidebarOptions: {
groupPathsBy: "tag",
categoryLinkSource: "tag",
categoryLinkSource: "auto",
},
},
user_v3: {
@ -323,7 +329,7 @@ module.exports = {
outputDir: "docs/apis/resources/user_service_v3",
sidebarOptions: {
groupPathsBy: "tag",
categoryLinkSource: "tag",
categoryLinkSource: "auto",
},
},
action_v3: {
@ -331,7 +337,7 @@ module.exports = {
outputDir: "docs/apis/resources/action_service_v3",
sidebarOptions: {
groupPathsBy: "tag",
categoryLinkSource: "tag",
categoryLinkSource: "auto",
},
},
feature_v2: {
@ -339,7 +345,7 @@ module.exports = {
outputDir: "docs/apis/resources/feature_service_v2",
sidebarOptions: {
groupPathsBy: "tag",
categoryLinkSource: "tag",
categoryLinkSource: "auto",
},
},
},
@ -358,5 +364,5 @@ module.exports = {
};
},
],
themes: ["@saucelabs/theme-github-codeblock", "docusaurus-theme-openapi-docs"],
themes: [ "docusaurus-theme-github-codeblock", "docusaurus-theme-openapi-docs"],
};

View File

@ -29,13 +29,13 @@
"@headlessui/react": "^1.7.4",
"@heroicons/react": "^2.0.13",
"@mdx-js/react": "^3.0.0",
"@saucelabs/theme-github-codeblock": "^0.2.3",
"@swc/core": "^1.3.74",
"autoprefixer": "^10.4.13",
"clsx": "^1.2.1",
"docusaurus-plugin-image-zoom": "^1.0.1",
"docusaurus-plugin-openapi-docs": "3.0.0-beta.10",
"docusaurus-theme-openapi-docs": "3.0.0-beta.10",
"docusaurus-plugin-openapi-docs": "3.0.1",
"docusaurus-theme-github-codeblock": "^2.0.2",
"docusaurus-theme-openapi-docs": "3.0.1",
"mdx-mermaid": "^2.0.0",
"mermaid": "^10.9.1",
"postcss": "^8.4.31",

View File

@ -121,7 +121,7 @@ module.exports = {
title: "Overview",
slug: "guides/manage/cloud/overview",
description:
"Our customer portal is used to manage all your ZITADEL instances. You can also manage your subscriptions, billing, newsletters and support requests.",
"Our customer portal is used to manage all your ZITADEL instances. You can also manage your subscriptions, billing, newsletters and support requests.",
},
items: [
"guides/manage/cloud/start",
@ -216,7 +216,7 @@ module.exports = {
"guides/integrate/login/login-users",
{
type: "category",
label: "Openid Connect",
label: "OpenID Connect",
collapsed: true,
link: {
type: "generated-index",
@ -232,24 +232,7 @@ module.exports = {
"guides/integrate/login/oidc/logout",
],
},
/*
{
type: "category",
label: "SAML",
collapsed: true,
link: {
type: "generated-index",
title: "Authenticate users with openid connect (OIDC)",
slug: "guides/integrate/login/saml",
description: ".",
},
items: [
{
type: "autogenerated",
dirName: "guides/integrate/login/saml",
},
],
},*/
"guides/integrate/login/saml",
],
},
{

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -1670,7 +1670,7 @@
fs-extra "^11.1.1"
tslib "^2.6.0"
"@docusaurus/types@3.4.0":
"@docusaurus/types@3.4.0", "@docusaurus/types@^3.0.0":
version "3.4.0"
resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-3.4.0.tgz#237c3f737e9db3f7c1a5935a3ef48d6eadde8292"
integrity sha512-4jcDO8kXi5Cf9TcyikB/yKmz14f2RZ2qTRerbHAsS+5InE9ZgSLBNLsewtFTcTOXSVcbU3FoGOzcNWAmU1TR0A==
@ -1737,6 +1737,11 @@
resolved "https://registry.yarnpkg.com/@exodus/schemasafe/-/schemasafe-1.3.0.tgz#731656abe21e8e769a7f70a4d833e6312fe59b7f"
integrity sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==
"@faker-js/faker@5.5.3":
version "5.5.3"
resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-5.5.3.tgz#18e3af6b8eae7984072bbeb0c0858474d7c4cefe"
integrity sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw==
"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0":
version "9.3.0"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb"
@ -1905,49 +1910,6 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@paloaltonetworks/openapi-to-postmanv2@3.1.0-hotfix.1":
version "3.1.0-hotfix.1"
resolved "https://registry.yarnpkg.com/@paloaltonetworks/openapi-to-postmanv2/-/openapi-to-postmanv2-3.1.0-hotfix.1.tgz#4baf401d2e94ba86d888e6011a4c45d824e88114"
integrity sha512-0bdaPCEyQbnUo4xpOu7EzxXXkDx4BAXqc8QSbVBlzlVB5KoTLJiKKB4c3fa4BXbK+3u/OqfLbeNCebc2EC8ngA==
dependencies:
"@paloaltonetworks/postman-collection" "^4.1.0"
ajv "8.1.0"
ajv-formats "2.1.1"
async "3.2.1"
commander "2.20.3"
js-yaml "3.14.1"
json-schema-merge-allof "0.8.1"
lodash "4.17.21"
oas-resolver-browser "2.5.2"
path-browserify "1.0.1"
yaml "1.10.2"
"@paloaltonetworks/postman-code-generators@1.1.15-patch.2":
version "1.1.15-patch.2"
resolved "https://registry.yarnpkg.com/@paloaltonetworks/postman-code-generators/-/postman-code-generators-1.1.15-patch.2.tgz#012051485269a2da6bd9a6b60031ddbc53e5e363"
integrity sha512-tRnAKtV4M8wLxcVnAx6ZCjCqbrR1xiqJNQkf1A71K8UxEP3N/+EspT82N5c0555w02oYFk21ViHuzuhm4gaGLw==
dependencies:
"@paloaltonetworks/postman-collection" "^4.1.0"
async "^3.2.4"
path "^0.12.7"
shelljs "^0.8.5"
"@paloaltonetworks/postman-collection@^4.1.0":
version "4.1.1"
resolved "https://registry.yarnpkg.com/@paloaltonetworks/postman-collection/-/postman-collection-4.1.1.tgz#b2130bc8d7396ea8e6a6b2e4642a6b224b41e1e1"
integrity sha512-9JHHkkD8Xb4rvdKob7TDPRfqfmdG3KU0aO5gJyyjvMFbOVysam5I0d8/9HPOuJXWkUHGo3Sn+ov2Fcm2bnJ52Q==
dependencies:
file-type "3.9.0"
http-reasons "0.1.0"
iconv-lite "0.6.3"
liquid-json "0.3.1"
lodash "4.17.21"
mime-format "2.0.1"
mime-types "2.1.34"
postman-url-encoder "3.0.5"
semver "7.3.5"
uuid "8.3.2"
"@pkgjs/parseargs@^0.11.0":
version "0.11.0"
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
@ -2020,11 +1982,6 @@
redux-thunk "^2.4.2"
reselect "^4.1.8"
"@saucelabs/theme-github-codeblock@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@saucelabs/theme-github-codeblock/-/theme-github-codeblock-0.2.3.tgz#706a43292f600532271979941b0155db667c2c21"
integrity sha512-GSl3Lr/jOWm4OP3BPX2vXxc8FMSOXj1mJnls6cUqMwlGOfKQ1Ia9pq1O9/ES+5TrZHIzAws/n5FFSn1OkGJw/Q==
"@sideway/address@^4.1.5":
version "4.1.5"
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5"
@ -2858,6 +2815,11 @@ aggregate-error@^3.0.0:
clean-stack "^2.0.0"
indent-string "^4.0.0"
ajv-draft-04@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz#3b64761b268ba0b9e668f0b41ba53fce0ad77fc8"
integrity sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==
ajv-formats@2.1.1, ajv-formats@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520"
@ -2877,10 +2839,10 @@ ajv-keywords@^5.1.0:
dependencies:
fast-deep-equal "^3.1.3"
ajv@8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.1.0.tgz#45d5d3d36c7cdd808930cc3e603cf6200dbeb736"
integrity sha512-B/Sk2Ix7A36fs/ZkuGLIR86EdjbgR6fsAcbx9lOP/QBSXujDNbVmIS/U4Itz5k8fPFDeVZl/zQ/gJW4Jrq6XjQ==
ajv@8.11.0:
version "8.11.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f"
integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==
dependencies:
fast-deep-equal "^3.1.1"
json-schema-traverse "^1.0.0"
@ -3041,15 +3003,15 @@ astring@^1.8.0:
resolved "https://registry.yarnpkg.com/astring/-/astring-1.8.6.tgz#2c9c157cf1739d67561c56ba896e6948f6b93731"
integrity sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==
async@3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.1.tgz#d3274ec66d107a47476a4c49136aacdb00665fc8"
integrity sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==
async@3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.2.tgz#2eb7671034bb2194d45d30e31e24ec7e7f9670cd"
integrity sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g==
async@^3.2.4:
version "3.2.5"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66"
integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==
async@3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c"
integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==
at-least-node@^1.0.0:
version "1.0.0"
@ -3426,11 +3388,6 @@ camelcase-css@^2.0.1:
resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
camelcase@^5.0.0:
version "5.3.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
camelcase@^6.2.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
@ -3607,15 +3564,6 @@ client-only@^0.0.1:
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
cliui@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
dependencies:
string-width "^4.2.0"
strip-ansi "^6.0.0"
wrap-ansi "^6.2.0"
cliui@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
@ -4473,11 +4421,6 @@ debug@4.3.4:
dependencies:
ms "2.1.2"
decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
decode-named-character-reference@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e"
@ -4665,17 +4608,15 @@ docusaurus-plugin-image-zoom@^1.0.1:
medium-zoom "^1.0.6"
validate-peer-dependencies "^2.2.0"
docusaurus-plugin-openapi-docs@3.0.0-beta.10, docusaurus-plugin-openapi-docs@^3.0.0-beta.10:
version "3.0.0-beta.10"
resolved "https://registry.yarnpkg.com/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-3.0.0-beta.10.tgz#f0c303ee852487c852c163d019678a92b53abf81"
integrity sha512-BtMBH4TzCiMM0WbO2ZAMXSuL7Ge9yyASZhAycb5vX+KSUnUgp47/Ex2f6/evBfaadnr6vXYEr1UBT1fSiJYh5w==
docusaurus-plugin-openapi-docs@3.0.1, docusaurus-plugin-openapi-docs@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/docusaurus-plugin-openapi-docs/-/docusaurus-plugin-openapi-docs-3.0.1.tgz#954fdc4103d7e47133aede210a98353b3e0f0f99"
integrity sha512-6SRqwey/TXMNu2G02mbWgxrifhpjGOjDr30N+58AR0Ytgc+HXMqlPAUIvTe+e7sOBfAtBbiNlmOWv5KSYIjf3w==
dependencies:
"@apidevtools/json-schema-ref-parser" "^11.5.4"
"@docusaurus/plugin-content-docs" "^3.0.1"
"@docusaurus/utils" "^3.0.1"
"@docusaurus/utils-validation" "^3.0.1"
"@paloaltonetworks/openapi-to-postmanv2" "3.1.0-hotfix.1"
"@paloaltonetworks/postman-collection" "^4.1.0"
"@redocly/openapi-core" "^1.10.5"
chalk "^4.1.2"
clsx "^1.1.1"
@ -4685,6 +4626,8 @@ docusaurus-plugin-openapi-docs@3.0.0-beta.10, docusaurus-plugin-openapi-docs@^3.
json5 "^2.2.3"
lodash "^4.17.20"
mustache "^4.2.0"
openapi-to-postmanv2 "^4.21.0"
postman-collection "^4.4.0"
slugify "^1.6.5"
swagger2openapi "^7.0.8"
xml-formatter "^2.6.1"
@ -4696,24 +4639,31 @@ docusaurus-plugin-sass@^0.2.3:
dependencies:
sass-loader "^10.1.1"
docusaurus-theme-openapi-docs@3.0.0-beta.10:
version "3.0.0-beta.10"
resolved "https://registry.yarnpkg.com/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-3.0.0-beta.10.tgz#f9a790b1ef88ff01f266224064f4b2f80d0892ff"
integrity sha512-8oUMMZSrRJ9EssrjWwbM9aYuHOt1AAm6wQDzWr8k6VvefGvVAibg4Y9PK7GeZ243lJikq9s45KqUA0SMwsm+Fg==
docusaurus-theme-github-codeblock@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/docusaurus-theme-github-codeblock/-/docusaurus-theme-github-codeblock-2.0.2.tgz#88b7044b81f9091330e8e4a07a1bdc9114a9fb93"
integrity sha512-H2WoQPWOLjGZO6KS58Gsd+eUVjTFJemkReiSSu9chqokyLc/3Ih3+zPRYfuEZ/HsDvSMIarf7CNcp+Vt+/G+ig==
dependencies:
"@docusaurus/types" "^3.0.0"
docusaurus-theme-openapi-docs@3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/docusaurus-theme-openapi-docs/-/docusaurus-theme-openapi-docs-3.0.1.tgz#49789c63377f294e624a9632eddb8265a421020f"
integrity sha512-tqypV91tC3wuWj9O+4n0M/e5AgHOeMT2nvPj1tjlPkC7/dLinZvpwQStT4YDUPYSoHRseqxd7lhivFQHcmlryg==
dependencies:
"@docusaurus/theme-common" "^3.0.1"
"@hookform/error-message" "^2.0.1"
"@paloaltonetworks/postman-code-generators" "1.1.15-patch.2"
"@paloaltonetworks/postman-collection" "^4.1.0"
"@reduxjs/toolkit" "^1.7.1"
clsx "^1.1.1"
copy-text-to-clipboard "^3.1.0"
crypto-js "^4.1.1"
docusaurus-plugin-openapi-docs "^3.0.0-beta.10"
docusaurus-plugin-openapi-docs "^3.0.1"
docusaurus-plugin-sass "^0.2.3"
file-saver "^2.0.5"
lodash "^4.17.20"
node-polyfill-webpack-plugin "^2.0.1"
postman-code-generators "^1.10.1"
postman-collection "^4.4.0"
prism-react-renderer "^2.3.0"
react-hook-form "^7.43.8"
react-live "^4.0.0"
@ -5205,6 +5155,11 @@ extract-zip@2.0.1:
optionalDependencies:
"@types/yauzl" "^2.9.1"
faker@5.5.3:
version "5.5.3"
resolved "https://registry.yarnpkg.com/faker/-/faker-5.5.3.tgz#c57974ee484431b25205c2c8dc09fda861e51e0e"
integrity sha512-wLTv2a28wjUyWkbnX7u/ABZBkUkIF2fCd73V6P2oFqEGEktDfzWx4UxrSqtPRw0xPRAcjeAOIiJWqZm3pP4u3g==
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@ -5336,14 +5291,6 @@ find-up@^3.0.0:
dependencies:
locate-path "^3.0.0"
find-up@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
dependencies:
locate-path "^5.0.0"
path-exists "^4.0.0"
find-up@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
@ -5483,7 +5430,7 @@ gensync@^1.0.0-beta.2:
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
get-caller-file@^2.0.1, get-caller-file@^2.0.5:
get-caller-file@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
@ -5648,6 +5595,13 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11,
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
graphlib@2.1.8:
version "2.1.8"
resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da"
integrity sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==
dependencies:
lodash "^4.17.15"
gray-matter@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.3.tgz#e893c064825de73ea1f5f7d88c7a9f7274288798"
@ -6609,7 +6563,14 @@ js-levenshtein@^1.1.6:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
js-yaml@3.14.1, js-yaml@^3.13.1:
js-yaml@4.1.0, js-yaml@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
dependencies:
argparse "^2.0.1"
js-yaml@^3.13.1:
version "3.14.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
@ -6617,13 +6578,6 @@ js-yaml@3.14.1, js-yaml@^3.13.1:
argparse "^1.0.7"
esprima "^4.0.0"
js-yaml@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
dependencies:
argparse "^2.0.1"
jsesc@^2.5.1:
version "2.5.2"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
@ -6807,13 +6761,6 @@ locate-path@^3.0.0:
p-locate "^3.0.0"
path-exists "^3.0.0"
locate-path@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
dependencies:
p-locate "^4.1.0"
locate-path@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
@ -6853,7 +6800,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
lodash@4.17.21, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4:
lodash@4.17.21, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -8044,10 +7991,10 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"
mime-db@1.51.0:
version "1.51.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c"
integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==
mime-db@1.48.0:
version "1.48.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d"
integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==
mime-db@1.52.0, "mime-db@>= 1.43.0 < 2":
version "1.52.0"
@ -8073,14 +8020,14 @@ mime-types@2.1.18:
dependencies:
mime-db "~1.33.0"
mime-types@2.1.34:
version "2.1.34"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24"
integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==
mime-types@2.1.31:
version "2.1.31"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.31.tgz#a00d76b74317c61f9c2db2218b8e9f8e9c5c9e6b"
integrity sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==
dependencies:
mime-db "1.51.0"
mime-db "1.48.0"
mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34:
mime-types@2.1.35, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
@ -8365,17 +8312,17 @@ oas-linter@^3.2.2:
should "^13.2.1"
yaml "^1.10.0"
oas-resolver-browser@2.5.2:
version "2.5.2"
resolved "https://registry.yarnpkg.com/oas-resolver-browser/-/oas-resolver-browser-2.5.2.tgz#d972525a840d7a74ab1aa43e215e9531a99412ba"
integrity sha512-L3ugWyBHOpKLT+lb+pFXCOpk3byh6usis5T9u9mfu92jH5bR6YK8MA2bebUTIjY7I4415PzDeZcmcc+i7X05MA==
oas-resolver-browser@2.5.6:
version "2.5.6"
resolved "https://registry.yarnpkg.com/oas-resolver-browser/-/oas-resolver-browser-2.5.6.tgz#1974db66d594fa8c67d3aa866b46b9e2156a8b55"
integrity sha512-Jw5elT/kwUJrnGaVuRWe1D7hmnYWB8rfDDjBnpQ+RYY/dzAewGXeTexXzt4fGEo6PUE4eqKqPWF79MZxxvMppA==
dependencies:
node-fetch-h2 "^2.3.0"
oas-kit-common "^1.0.8"
path-browserify "^1.0.1"
reftools "^1.1.6"
reftools "^1.1.9"
yaml "^1.10.0"
yargs "^15.3.1"
yargs "^17.0.1"
oas-resolver@^2.5.6:
version "2.5.6"
@ -8412,7 +8359,7 @@ object-assign@^4.0.1, object-assign@^4.1.1:
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
object-hash@^3.0.0:
object-hash@3.0.0, object-hash@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9"
integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==
@ -8485,6 +8432,28 @@ open@^8.0.9, open@^8.4.0:
is-docker "^2.1.1"
is-wsl "^2.2.0"
openapi-to-postmanv2@^4.21.0:
version "4.21.0"
resolved "https://registry.yarnpkg.com/openapi-to-postmanv2/-/openapi-to-postmanv2-4.21.0.tgz#4bc5b19ccbd1514c2b3466268a7f5dd64b61f535"
integrity sha512-UyHf2xOjDfl4bIaYrUP6w4UiR894gsiOt1HR93cWOxv33Ucw4rGG0B6ewQczSGBpvFMgri+KSSykNEVjsmB55w==
dependencies:
ajv "8.11.0"
ajv-draft-04 "1.0.0"
ajv-formats "2.1.1"
async "3.2.4"
commander "2.20.3"
graphlib "2.1.8"
js-yaml "4.1.0"
json-schema-merge-allof "0.8.1"
lodash "4.17.21"
oas-resolver-browser "2.5.6"
object-hash "3.0.0"
path-browserify "1.0.1"
postman-collection "4.2.1"
swagger2openapi "7.0.8"
traverse "0.6.6"
yaml "1.10.2"
opener@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
@ -8500,7 +8469,7 @@ p-cancelable@^3.0.0:
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-3.0.0.tgz#63826694b54d61ca1c20ebcb6d3ecf5e14cd8050"
integrity sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==
p-limit@^2.0.0, p-limit@^2.2.0:
p-limit@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
@ -8528,13 +8497,6 @@ p-locate@^3.0.0:
dependencies:
p-limit "^2.0.0"
p-locate@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
dependencies:
p-limit "^2.2.0"
p-locate@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
@ -8755,7 +8717,7 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
path@^0.12.7:
path@0.12.7:
version "0.12.7"
resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f"
integrity sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==
@ -9152,6 +9114,75 @@ postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.24, postcss@^8.4.26, postcss@^8.4
picocolors "^1.0.0"
source-map-js "^1.2.0"
postman-code-generators@^1.10.1:
version "1.10.1"
resolved "https://registry.yarnpkg.com/postman-code-generators/-/postman-code-generators-1.10.1.tgz#5d8d8500616b2bb0cac7417e923c36b2e73cbffe"
integrity sha512-VGjqIFezG6tZRP3GvSbYjLDq3bQtXUeNDnBrN5CUIbgc9siu5Ubkva9hZnFpk+/qfi+PXs3CUR5mvV+WzpMhsg==
dependencies:
async "3.2.2"
lodash "4.17.21"
path "0.12.7"
postman-collection "4.0.0"
shelljs "0.8.5"
postman-collection@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/postman-collection/-/postman-collection-4.0.0.tgz#4a72c60f0d9725656d0e02d44e294e1c22ef3ffa"
integrity sha512-vDrXG/dclSu6RMqPqBz4ZqoQBwcj/a80sJYsQZmzWJ6dWgXiudPhwu6Vm3C1Hy7zX5W8A6am1Z6vb/TB4eyURA==
dependencies:
faker "5.5.3"
file-type "3.9.0"
http-reasons "0.1.0"
iconv-lite "0.6.3"
liquid-json "0.3.1"
lodash "4.17.21"
mime-format "2.0.1"
mime-types "2.1.31"
postman-url-encoder "3.0.1"
semver "7.3.5"
uuid "8.3.2"
postman-collection@4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/postman-collection/-/postman-collection-4.2.1.tgz#86e3c8ee11530a1f00a30864503d487846ee516f"
integrity sha512-DFLt3/yu8+ldtOTIzmBUctoupKJBOVK4NZO0t68K2lIir9smQg7OdQTBjOXYy+PDh7u0pSDvD66tm93eBHEPHA==
dependencies:
"@faker-js/faker" "5.5.3"
file-type "3.9.0"
http-reasons "0.1.0"
iconv-lite "0.6.3"
liquid-json "0.3.1"
lodash "4.17.21"
mime-format "2.0.1"
mime-types "2.1.35"
postman-url-encoder "3.0.5"
semver "7.5.4"
uuid "8.3.2"
postman-collection@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/postman-collection/-/postman-collection-4.4.0.tgz#6acb6e3796fcd9f6ac5a94e6894185e42387d7da"
integrity sha512-2BGDFcUwlK08CqZFUlIC8kwRJueVzPjZnnokWPtJCd9f2J06HBQpGL7t2P1Ud1NEsK9NHq9wdipUhWLOPj5s/Q==
dependencies:
"@faker-js/faker" "5.5.3"
file-type "3.9.0"
http-reasons "0.1.0"
iconv-lite "0.6.3"
liquid-json "0.3.1"
lodash "4.17.21"
mime-format "2.0.1"
mime-types "2.1.35"
postman-url-encoder "3.0.5"
semver "7.5.4"
uuid "8.3.2"
postman-url-encoder@3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/postman-url-encoder/-/postman-url-encoder-3.0.1.tgz#a7434a169567c45f022dc435d46a86d71de6a8aa"
integrity sha512-dMPqXnkDlstM2Eya+Gw4MIGWEan8TzldDcUKZIhZUsJ/G5JjubfQPhFhVWKzuATDMvwvrWbSjF+8VmAvbu6giw==
dependencies:
punycode "^2.1.1"
postman-url-encoder@3.0.5:
version "3.0.5"
resolved "https://registry.yarnpkg.com/postman-url-encoder/-/postman-url-encoder-3.0.5.tgz#af2efee3bb7644e2b059d8a78bc8070fae0467a5"
@ -9700,7 +9731,7 @@ redux@^4.0.0, redux@^4.2.1:
dependencies:
"@babel/runtime" "^7.9.2"
reftools@^1.1.6, reftools@^1.1.9:
reftools@^1.1.9:
version "1.1.9"
resolved "https://registry.yarnpkg.com/reftools/-/reftools-1.1.9.tgz#e16e19f662ccd4648605312c06d34e5da3a2b77e"
integrity sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==
@ -9911,11 +9942,6 @@ require-from-string@^2.0.2:
resolved "https://registry.yarnpkg.com/require-like/-/require-like-0.1.2.tgz#ad6f30c13becd797010c468afa775c0c0a6b47fa"
integrity sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==
require-main-filename@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
requires-port@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
@ -10152,6 +10178,13 @@ semver@7.3.5:
dependencies:
lru-cache "^6.0.0"
semver@7.5.4:
version "7.5.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
dependencies:
lru-cache "^6.0.0"
semver@^6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
@ -10225,11 +10258,6 @@ serve-static@1.15.0:
parseurl "~1.3.3"
send "0.18.0"
set-blocking@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
set-function-length@^1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
@ -10294,7 +10322,7 @@ shell-quote@^1.7.3, shell-quote@^1.8.1:
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680"
integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==
shelljs@^0.8.5:
shelljs@0.8.5, shelljs@^0.8.5:
version "0.8.5"
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c"
integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==
@ -10722,7 +10750,7 @@ svgo@^3.0.2, svgo@^3.2.0:
csso "^5.0.5"
picocolors "^1.0.0"
swagger2openapi@^7.0.8:
swagger2openapi@7.0.8, swagger2openapi@^7.0.8:
version "7.0.8"
resolved "https://registry.yarnpkg.com/swagger2openapi/-/swagger2openapi-7.0.8.tgz#12c88d5de776cb1cbba758994930f40ad0afac59"
integrity sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==
@ -10904,6 +10932,11 @@ tr46@~0.0.3:
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
traverse@0.6.6:
version "0.6.6"
resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137"
integrity sha512-kdf4JKs8lbARxWdp7RKdNzoJBhGUcIalSYibuGyHJbmk40pOysQ0+QPvlkCOICOivDWU2IJo2rkrxyTK2AH4fw==
trim-lines@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338"
@ -11562,11 +11595,6 @@ whatwg-url@^5.0.0:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
which-module@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409"
integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==
which-typed-array@^1.1.14, which-typed-array@^1.1.2:
version "1.1.15"
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d"
@ -11613,15 +11641,6 @@ wildcard@^2.0.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
@ -11699,11 +11718,6 @@ xtend@^4.0.2:
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
y18n@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
y18n@^5.0.5:
version "5.0.8"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
@ -11734,36 +11748,11 @@ yaml@^2.3.4:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.5.tgz#60630b206dd6d84df97003d33fc1ddf6296cca5e"
integrity sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==
yargs-parser@^18.1.2:
version "18.1.3"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
dependencies:
camelcase "^5.0.0"
decamelize "^1.2.0"
yargs-parser@^21.1.1:
version "21.1.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
yargs@^15.3.1:
version "15.4.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
dependencies:
cliui "^6.0.0"
decamelize "^1.2.0"
find-up "^4.1.0"
get-caller-file "^2.0.1"
require-directory "^2.1.1"
require-main-filename "^2.0.0"
set-blocking "^2.0.0"
string-width "^4.2.0"
which-module "^2.0.0"
y18n "^4.0.0"
yargs-parser "^18.1.2"
yargs@^17.0.1:
version "17.7.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"

View File

@ -0,0 +1,224 @@
const secretGeneratorSettingsPath = `/instance?id=secrets`;
beforeEach(() => {
cy.context().as('ctx');
});
describe('instance secret generators', () => {
describe('secret generator settings', () => {
it(`should show secret generator cards`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.contains('Initialization Mail');
cy.contains('Email verification');
cy.contains('Phone verification');
cy.contains('Password Reset');
cy.contains('Passwordless Initialization');
cy.contains('App Secret');
cy.contains('One Time Password (OTP) - SMS');
cy.contains('One Time Password (OTP) - Email');
});
it(`Initialization Mail should contain default settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.get('input[id="length1"]').should('have.value', '6');
cy.get('input[id="expiry1"]').should('have.value', '4320');
cy.get('mat-slide-toggle#includeLowerLetters1 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeUpperLetters1 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeDigits1 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols1 button').should('have.attr', 'aria-checked', 'false');
});
it(`Email verification should contain default settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.get('input[id="length2"]').should('have.value', '6');
cy.get('input[id="expiry2"]').should('have.value', '60');
cy.get('mat-slide-toggle#includeLowerLetters2 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeUpperLetters2 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeDigits2 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols2 button').should('have.attr', 'aria-checked', 'false');
});
it(`Phone verification should contain default settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.get('input[id="length3"]').should('have.value', '6');
cy.get('input[id="expiry3"]').should('have.value', '60');
cy.get('mat-slide-toggle#includeLowerLetters3 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeUpperLetters3 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeDigits3 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols3 button').should('have.attr', 'aria-checked', 'false');
});
it(`Password Reset should contain default settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.get('input[id="length4"]').should('have.value', '6');
cy.get('input[id="expiry4"]').should('have.value', '60');
cy.get('mat-slide-toggle#includeLowerLetters4 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeUpperLetters4 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeDigits4 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols4 button').should('have.attr', 'aria-checked', 'false');
});
it(`Passwordless Initialization should contain default settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.get('input[id="length5"]').should('have.value', '12');
cy.get('input[id="expiry5"]').should('have.value', '60');
cy.get('mat-slide-toggle#includeLowerLetters5 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeUpperLetters5 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeDigits5 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols5 button').should('have.attr', 'aria-checked', 'false');
});
it(`App Secret should contain default settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.get('input[id="length6"]').should('have.value', '64');
cy.get('input[id="expiry6"]').should('have.value', '0');
cy.get('mat-slide-toggle#includeLowerLetters6 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeUpperLetters6 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeDigits6 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols6 button').should('have.attr', 'aria-checked', 'false');
});
it(`One Time Password (OTP) - SMS should contain default settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.get('input[id="length7"]').should('have.value', '8');
cy.get('input[id="expiry7"]').should('have.value', '5');
cy.get('mat-slide-toggle#includeLowerLetters7 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeUpperLetters7 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeDigits7 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols7 button').should('have.attr', 'aria-checked', 'false');
});
it(`One Time Password (OTP) - Email should contain default settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.get('input[id="length8"]').should('have.value', '8');
cy.get('input[id="expiry8"]').should('have.value', '5');
cy.get('mat-slide-toggle#includeLowerLetters8 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeUpperLetters8 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeDigits8 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols8 button').should('have.attr', 'aria-checked', 'false');
});
it(`Initialization Mail should update settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.wait(1000);
cy.get('input[id="length1"]').clear().type('64');
cy.get('mat-slide-toggle#includeLowerLetters1 button').click();
cy.get('button[id="saveSecretGenerator1"]').click();
cy.wait(1000);
cy.get('input[id="length1"]').should('have.value', '64');
cy.get('input[id="expiry1"]').should('have.value', '4320');
cy.get('mat-slide-toggle#includeLowerLetters1 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeUpperLetters1 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeDigits1 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols1 button').should('have.attr', 'aria-checked', 'false');
});
it(`Email verification should update settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.wait(1000);
cy.get('input[id="length2"]').clear().type('64');
cy.get('mat-slide-toggle#includeUpperLetters2 button').click();
cy.get('button[id="saveSecretGenerator2"]').click();
cy.wait(1000);
cy.get('input[id="length2"]').should('have.value', '64');
cy.get('input[id="expiry2"]').should('have.value', '60');
cy.get('mat-slide-toggle#includeLowerLetters2 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeUpperLetters2 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeDigits2 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols2 button').should('have.attr', 'aria-checked', 'false');
});
it(`Phone verification should update settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.wait(1000);
cy.get('input[id="expiry3"]').clear().type('10');
cy.get('mat-slide-toggle#includeSymbols3 button').click();
cy.get('button[id="saveSecretGenerator3"]').click();
cy.wait(1000);
cy.get('input[id="length3"]').should('have.value', '6');
cy.get('input[id="expiry3"]').should('have.value', '10');
cy.get('mat-slide-toggle#includeLowerLetters3 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeUpperLetters3 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeDigits3 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols3 button').should('have.attr', 'aria-checked', 'true');
});
it(`Password Reset should update settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.wait(1000);
cy.get('input[id="expiry4"]').clear().type('5');
cy.get('mat-slide-toggle#includeDigits4 button').click();
cy.get('button[id="saveSecretGenerator4"]').click();
cy.wait(1000);
cy.get('input[id="length4"]').should('have.value', '6');
cy.get('input[id="expiry4"]').should('have.value', '5');
cy.get('mat-slide-toggle#includeLowerLetters4 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeUpperLetters4 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeDigits4 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeSymbols4 button').should('have.attr', 'aria-checked', 'false');
});
it(`Passwordless Initialization should update settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.wait(1000);
cy.get('input[id="length5"]').clear().type('64');
cy.get('mat-slide-toggle#includeDigits5 button').click();
cy.get('button[id="saveSecretGenerator5"]').click();
cy.wait(1000);
cy.get('input[id="length5"]').should('have.value', '64');
cy.get('input[id="expiry5"]').should('have.value', '60');
cy.get('mat-slide-toggle#includeLowerLetters5 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeUpperLetters5 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeDigits5 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeSymbols5 button').should('have.attr', 'aria-checked', 'false');
});
it(`App Secret should update settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.wait(1000);
cy.get('input[id="length6"]').clear().type('32');
cy.get('input[id="expiry6"]').clear().type('120');
cy.get('mat-slide-toggle#includeUpperLetters6 button').click();
cy.get('button[id="saveSecretGenerator6"]').click();
cy.wait(1000);
cy.get('input[id="length6"]').should('have.value', '32');
cy.get('input[id="expiry6"]').should('have.value', '120');
cy.get('mat-slide-toggle#includeLowerLetters6 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeUpperLetters6 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeDigits6 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols6 button').should('have.attr', 'aria-checked', 'false');
});
it(`One Time Password (OTP) - SMS should update settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.wait(1000);
cy.get('input[id="expiry7"]').clear().type('120');
cy.get('mat-slide-toggle#includeLowerLetters7 button').click();
cy.get('button[id="saveSecretGenerator7"]').click();
cy.wait(1000);
cy.get('input[id="length7"]').should('have.value', '8');
cy.get('input[id="expiry7"]').should('have.value', '120');
cy.get('mat-slide-toggle#includeLowerLetters7 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeUpperLetters7 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeDigits7 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols7 button').should('have.attr', 'aria-checked', 'false');
});
it(`One Time Password (OTP) should update settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.wait(1000);
cy.get('input[id="length8"]').clear().type('12');
cy.get('input[id="expiry8"]').clear().type('90');
cy.get('mat-slide-toggle#includeDigits8 button').click();
cy.get('mat-slide-toggle#includeSymbols8 button').click();
cy.get('button[id="saveSecretGenerator8"]').click();
cy.wait(1000);
cy.get('input[id="length8"]').should('have.value', '12');
cy.get('input[id="expiry8"]').should('have.value', '90');
cy.get('mat-slide-toggle#includeLowerLetters8 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeUpperLetters8 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeDigits8 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeSymbols8 button').should('have.attr', 'aria-checked', 'true');
});
});
});

View File

@ -50,7 +50,7 @@ func (s *Server) VerifyMyEmail(ctx context.Context, req *auth_pb.VerifyMyEmailRe
return nil, err
}
ctxData := authz.GetCtxData(ctx)
objectDetails, err := s.command.VerifyHumanEmail(ctx, ctxData.UserID, req.Code, ctxData.ResourceOwner, emailCodeGenerator)
objectDetails, err := s.command.VerifyHumanEmail(ctx, ctxData.UserID, req.Code, ctxData.ResourceOwner, "", "", emailCodeGenerator)
if err != nil {
return nil, err
}

View File

@ -260,7 +260,7 @@ func appendIfNotExists(array []string, value string) []string {
func ListMyProjectOrgsRequestToQuery(req *auth_pb.ListMyProjectOrgsRequest) (*query.OrgSearchQueries, error) {
offset, limit, asc := obj_grpc.ListQueryToModel(req.Query)
queries, err := org.OrgQueriesToQuery(req.Queries)
queries, err := org.OrgQueriesToModel(req.Queries)
if err != nil {
return nil, err
}

View File

@ -586,11 +586,7 @@ func (s *Server) SetHumanPassword(ctx context.Context, req *mgmt_pb.SetHumanPass
}
func (s *Server) SendHumanResetPasswordNotification(ctx context.Context, req *mgmt_pb.SendHumanResetPasswordNotificationRequest) (*mgmt_pb.SendHumanResetPasswordNotificationResponse, error) {
passwordCodeGenerator, err := s.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypePasswordResetCode, s.userCodeAlg)
if err != nil {
return nil, err
}
objectDetails, err := s.command.RequestSetPassword(ctx, req.UserId, authz.GetCtxData(ctx).OrgID, notifyTypeToDomain(req.Type), passwordCodeGenerator, "")
objectDetails, err := s.command.RequestSetPassword(ctx, req.UserId, authz.GetCtxData(ctx).OrgID, notifyTypeToDomain(req.Type), "")
if err != nil {
return nil, err
}

View File

@ -133,8 +133,14 @@ func ImportHumanUserRequestToDomain(req *mgmt_pb.ImportHumanUserRequest) (human
}
func AddMachineUserRequestToCommand(req *mgmt_pb.AddMachineUserRequest, resourceowner string) *command.Machine {
userId := ""
if req.UserId != nil {
userId = *req.UserId
}
return &command.Machine{
ObjectRoot: models.ObjectRoot{
AggregateID: userId,
ResourceOwner: resourceowner,
},
Username: req.UserName,

View File

@ -78,3 +78,36 @@ func TestImport_UnparsablePreferredLanguage(t *testing.T) {
})
require.NoError(t, err)
}
func TestAdd_MachineUser(t *testing.T) {
random := integration.RandString(5)
res, err := Client.AddMachineUser(OrgCTX, &management.AddMachineUserRequest{
UserName: random,
Name: "testMachineName1",
Description: "testMachineDescription1",
AccessTokenType: 0,
})
require.NoError(t, err)
_, err = Client.GetUserByID(OrgCTX, &management.GetUserByIDRequest{Id: res.GetUserId()})
require.NoError(t, err)
}
func TestAdd_MachineUserCustomID(t *testing.T) {
id := integration.RandString(5)
random := integration.RandString(5)
res, err := Client.AddMachineUser(OrgCTX, &management.AddMachineUserRequest{
UserId: &id,
UserName: random,
Name: "testMachineName1",
Description: "testMachineDescription1",
AccessTokenType: 0,
})
require.NoError(t, err)
_, err = Client.GetUserByID(OrgCTX, &management.GetUserByIDRequest{Id: id})
require.NoError(t, err)
require.Equal(t, id, res.GetUserId())
}

View File

@ -27,35 +27,13 @@ func OrgQueryToModel(apiQuery *org_pb.OrgQuery) (query.SearchQuery, error) {
return query.NewOrgNameSearchQuery(object.TextMethodToQuery(q.NameQuery.Method), q.NameQuery.Name)
case *org_pb.OrgQuery_StateQuery:
return query.NewOrgStateSearchQuery(OrgStateToDomain(q.StateQuery.State))
case *org_pb.OrgQuery_IdQuery:
return query.NewOrgIDSearchQuery(q.IdQuery.Id)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "ORG-vR9nC", "List.Query.Invalid")
}
}
func OrgQueriesToQuery(queries []*org_pb.OrgQuery) (_ []query.SearchQuery, err error) {
q := make([]query.SearchQuery, len(queries))
for i, query := range queries {
q[i], err = OrgQueryToQuery(query)
if err != nil {
return nil, err
}
}
return q, nil
}
func OrgQueryToQuery(search *org_pb.OrgQuery) (query.SearchQuery, error) {
switch q := search.Query.(type) {
case *org_pb.OrgQuery_DomainQuery:
return query.NewOrgDomainSearchQuery(object.TextMethodToQuery(q.DomainQuery.Method), q.DomainQuery.Domain)
case *org_pb.OrgQuery_NameQuery:
return query.NewOrgNameSearchQuery(object.TextMethodToQuery(q.NameQuery.Method), q.NameQuery.Name)
case *org_pb.OrgQuery_StateQuery:
return query.NewOrgStateSearchQuery(OrgStateToDomain(q.StateQuery.State))
default:
return nil, zerrors.ThrowInvalidArgument(nil, "ADMIN-ADvsd", "List.Query.Invalid")
}
}
func OrgViewsToPb(orgs []*query.Org) []*org_pb.Org {
o := make([]*org_pb.Org, len(orgs))
for i, org := range orgs {

View File

@ -40,6 +40,23 @@ func (s *Server) SetPhone(ctx context.Context, req *user.SetPhoneRequest) (resp
}, nil
}
func (s *Server) RemovePhone(ctx context.Context, req *user.RemovePhoneRequest) (resp *user.RemovePhoneResponse, err error) {
details, err := s.command.RemoveUserPhone(ctx,
req.GetUserId(),
)
if err != nil {
return nil, err
}
return &user.RemovePhoneResponse{
Details: &object.Details{
Sequence: details.Sequence,
ChangeDate: timestamppb.New(details.EventDate),
ResourceOwner: details.ResourceOwner,
},
}, nil
}
func (s *Server) ResendPhoneCode(ctx context.Context, req *user.ResendPhoneCodeRequest) (resp *user.ResendPhoneCodeResponse, err error) {
var phone *domain.Phone
switch v := req.GetVerification().(type) {

View File

@ -3,6 +3,7 @@
package user_test
import (
"context"
"fmt"
"testing"
"time"
@ -245,3 +246,99 @@ func TestServer_VerifyPhone(t *testing.T) {
})
}
}
func TestServer_RemovePhone(t *testing.T) {
userResp := Tester.CreateHumanUser(CTX)
failResp := Tester.CreateHumanUserNoPhone(CTX)
otherUser := Tester.CreateHumanUser(CTX).GetUserId()
doubleRemoveUser := Tester.CreateHumanUser(CTX)
Tester.RegisterUserPasskey(CTX, otherUser)
_, sessionTokenOtherUser, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, otherUser)
tests := []struct {
name string
ctx context.Context
req *user.RemovePhoneRequest
want *user.RemovePhoneResponse
wantErr bool
dep func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error)
}{
{
name: "remove phone",
ctx: CTX,
req: &user.RemovePhoneRequest{
UserId: userResp.GetUserId(),
},
want: &user.RemovePhoneResponse{
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.Now(),
ResourceOwner: Tester.Organisation.ID,
},
},
dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) {
return nil, nil
},
},
{
name: "user without phone",
ctx: CTX,
req: &user.RemovePhoneRequest{
UserId: failResp.GetUserId(),
},
wantErr: true,
dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) {
return nil, nil
},
},
{
name: "remove previously deleted phone",
ctx: CTX,
req: &user.RemovePhoneRequest{
UserId: doubleRemoveUser.GetUserId(),
},
wantErr: true,
dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) {
return Client.RemovePhone(ctx, &user.RemovePhoneRequest{
UserId: doubleRemoveUser.GetUserId(),
});
},
},
{
name: "no user id",
ctx: CTX,
req: &user.RemovePhoneRequest{},
wantErr: true,
dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) {
return nil, nil
},
},
{
name: "other user, no permission",
ctx: Tester.WithAuthorizationToken(CTX, sessionTokenOtherUser),
req: &user.RemovePhoneRequest{
UserId: userResp.GetUserId(),
},
wantErr: true,
dep: func(ctx context.Context, userID string) (*user.RemovePhoneResponse, error) {
return nil, nil
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, depErr := tt.dep(tt.ctx, tt.req.UserId)
require.NoError(t, depErr)
got, err := Client.RemovePhone(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
integration.AssertDetails(t, tt.want, got)
})
}
}

View File

@ -60,7 +60,12 @@ func UsersToPb(users []*query.User, assetPrefix string) []*user.User {
func userToPb(userQ *query.User, assetPrefix string) *user.User {
return &user.User{
UserId: userQ.ID,
UserId: userQ.ID,
Details: object.DomainToDetailsPb(&domain.ObjectDetails{
Sequence: userQ.Sequence,
EventDate: userQ.ChangeDate,
ResourceOwner: userQ.ResourceOwner,
}),
State: userStateToPb(userQ.State),
Username: userQ.Username,
LoginNames: userQ.LoginNames,

View File

@ -23,7 +23,7 @@ func TestServer_GetUserByID(t *testing.T) {
type args struct {
ctx context.Context
req *user.GetUserByIDRequest
dep func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*timestamppb.Timestamp, error)
dep func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*userAttr, error)
}
tests := []struct {
name string
@ -38,7 +38,7 @@ func TestServer_GetUserByID(t *testing.T) {
&user.GetUserByIDRequest{
UserId: "",
},
func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*timestamppb.Timestamp, error) {
func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*userAttr, error) {
return nil, nil
},
},
@ -51,7 +51,7 @@ func TestServer_GetUserByID(t *testing.T) {
&user.GetUserByIDRequest{
UserId: "unknown",
},
func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*timestamppb.Timestamp, error) {
func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*userAttr, error) {
return nil, nil
},
},
@ -62,10 +62,10 @@ func TestServer_GetUserByID(t *testing.T) {
args: args{
IamCTX,
&user.GetUserByIDRequest{},
func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*timestamppb.Timestamp, error) {
func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*userAttr, error) {
resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
request.UserId = resp.GetUserId()
return nil, nil
return &userAttr{resp.GetUserId(), username, nil, resp.GetDetails()}, nil
},
},
want: &user.GetUserByIDResponse{
@ -106,11 +106,11 @@ func TestServer_GetUserByID(t *testing.T) {
args: args{
IamCTX,
&user.GetUserByIDRequest{},
func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*timestamppb.Timestamp, error) {
func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*userAttr, error) {
resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
request.UserId = resp.GetUserId()
changed := Tester.SetUserPassword(ctx, resp.GetUserId(), integration.UserPassword, true)
return changed, nil
details := Tester.SetUserPassword(ctx, resp.GetUserId(), integration.UserPassword, true)
return &userAttr{resp.GetUserId(), username, details.GetChangeDate(), resp.GetDetails()}, nil
},
},
want: &user.GetUserByIDResponse{
@ -152,7 +152,7 @@ func TestServer_GetUserByID(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
username := fmt.Sprintf("%d@mouse.com", time.Now().UnixNano())
changed, err := tt.args.dep(tt.args.ctx, username, tt.args.req)
userAttr, err := tt.args.dep(tt.args.ctx, username, tt.args.req)
require.NoError(t, err)
retryDuration := time.Minute
if ctxDeadline, ok := CTX.Deadline(); ok {
@ -168,14 +168,15 @@ func TestServer_GetUserByID(t *testing.T) {
if getErr != nil {
return
}
tt.want.User.UserId = tt.args.req.GetUserId()
tt.want.User.Username = username
tt.want.User.PreferredLoginName = username
tt.want.User.LoginNames = []string{username}
tt.want.User.Details = userAttr.Details
tt.want.User.UserId = userAttr.UserID
tt.want.User.Username = userAttr.Username
tt.want.User.PreferredLoginName = userAttr.Username
tt.want.User.LoginNames = []string{userAttr.Username}
if human := tt.want.User.GetHuman(); human != nil {
human.Email.Email = username
human.Email.Email = userAttr.Username
if tt.want.User.GetHuman().GetPasswordChanged() != nil {
human.PasswordChanged = changed
human.PasswordChanged = userAttr.Changed
}
}
assert.Equal(ttt, tt.want.User, got.User)
@ -311,6 +312,9 @@ func TestServer_GetUserByID_Permission(t *testing.T) {
if human := tt.want.User.GetHuman(); human != nil {
human.Email.Email = newOrgOwnerEmail
}
// details tested in GetUserByID
tt.want.User.Details = got.User.GetDetails()
assert.Equal(t, tt.want.User, got.User)
}
})
@ -321,6 +325,7 @@ type userAttr struct {
UserID string
Username string
Changed *timestamppb.Timestamp
Details *object.Details
}
func TestServer_ListUsers(t *testing.T) {
@ -374,7 +379,7 @@ func TestServer_ListUsers(t *testing.T) {
for i, username := range usernames {
resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
userIDs[i] = resp.GetUserId()
infos[i] = userAttr{resp.GetUserId(), username, nil}
infos[i] = userAttr{resp.GetUserId(), username, nil, resp.GetDetails()}
}
request.Queries = append(request.Queries, InUserIDsQuery(userIDs))
return infos, nil
@ -428,8 +433,8 @@ func TestServer_ListUsers(t *testing.T) {
for i, username := range usernames {
resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
userIDs[i] = resp.GetUserId()
changed := Tester.SetUserPassword(ctx, resp.GetUserId(), integration.UserPassword, true)
infos[i] = userAttr{resp.GetUserId(), username, changed}
details := Tester.SetUserPassword(ctx, resp.GetUserId(), integration.UserPassword, true)
infos[i] = userAttr{resp.GetUserId(), username, details.GetChangeDate(), resp.GetDetails()}
}
request.Queries = append(request.Queries, InUserIDsQuery(userIDs))
return infos, nil
@ -485,7 +490,7 @@ func TestServer_ListUsers(t *testing.T) {
for i, username := range usernames {
resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
userIDs[i] = resp.GetUserId()
infos[i] = userAttr{resp.GetUserId(), username, nil}
infos[i] = userAttr{resp.GetUserId(), username, nil, resp.GetDetails()}
}
request.Queries = append(request.Queries, InUserIDsQuery(userIDs))
return infos, nil
@ -581,7 +586,7 @@ func TestServer_ListUsers(t *testing.T) {
for i, username := range usernames {
resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
userIDs[i] = resp.GetUserId()
infos[i] = userAttr{resp.GetUserId(), username, nil}
infos[i] = userAttr{resp.GetUserId(), username, nil, resp.GetDetails()}
request.Queries = append(request.Queries, UsernameQuery(username))
}
return infos, nil
@ -633,7 +638,7 @@ func TestServer_ListUsers(t *testing.T) {
infos := make([]userAttr, len(usernames))
for i, username := range usernames {
resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
infos[i] = userAttr{resp.GetUserId(), username, nil}
infos[i] = userAttr{resp.GetUserId(), username, nil, resp.GetDetails()}
}
request.Queries = append(request.Queries, InUserEmailsQuery(usernames))
return infos, nil
@ -685,7 +690,7 @@ func TestServer_ListUsers(t *testing.T) {
infos := make([]userAttr, len(usernames))
for i, username := range usernames {
resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
infos[i] = userAttr{resp.GetUserId(), username, nil}
infos[i] = userAttr{resp.GetUserId(), username, nil, resp.GetDetails()}
}
request.Queries = append(request.Queries, InUserEmailsQuery(usernames))
return infos, nil
@ -800,7 +805,7 @@ func TestServer_ListUsers(t *testing.T) {
infos := make([]userAttr, len(usernames))
for i, username := range usernames {
resp := Tester.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
infos[i] = userAttr{resp.GetUserId(), username, nil}
infos[i] = userAttr{resp.GetUserId(), username, nil, resp.GetDetails()}
}
request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId))
request.Queries = append(request.Queries, InUserEmailsQuery(usernames))
@ -920,6 +925,7 @@ func TestServer_ListUsers(t *testing.T) {
human.PasswordChanged = infos[i].Changed
}
}
tt.want.Result[i].Details = infos[i].Details
}
for i := range tt.want.Result {
assert.Contains(ttt, got.Result, tt.want.Result[i])

View File

@ -12,7 +12,6 @@ func (s *Server) RegisterTOTP(ctx context.Context, req *user.RegisterTOTPRequest
return totpDetailsToPb(
s.command.AddUserTOTP(ctx, req.GetUserId(), ""),
)
}
func totpDetailsToPb(totp *domain.TOTP, err error) (*user.RegisterTOTPResponse, error) {
@ -35,3 +34,11 @@ func (s *Server) VerifyTOTPRegistration(ctx context.Context, req *user.VerifyTOT
Details: object.DomainToDetailsPb(objectDetails),
}, nil
}
func (s *Server) RemoveTOTP(ctx context.Context, req *user.RemoveTOTPRequest) (*user.RemoveTOTPResponse, error) {
objectDetails, err := s.command.HumanRemoveTOTP(ctx, req.GetUserId(), "")
if err != nil {
return nil, err
}
return &user.RemoveTOTPResponse{Details: object.DomainToDetailsPb(objectDetails)}, nil
}

View File

@ -205,3 +205,80 @@ func TestServer_VerifyTOTPRegistration(t *testing.T) {
})
}
}
func TestServer_RemoveTOTP(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, userID)
_, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
userVerified := Tester.CreateHumanUser(CTX)
Tester.RegisterUserPasskey(CTX, userVerified.GetUserId())
_, sessionTokenVerified, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userVerified.GetUserId())
userVerifiedCtx := Tester.WithAuthorizationToken(context.Background(), sessionTokenVerified)
_, err := Tester.Client.UserV2.VerifyPhone(userVerifiedCtx, &user.VerifyPhoneRequest{
UserId: userVerified.GetUserId(),
VerificationCode: userVerified.GetPhoneCode(),
})
require.NoError(t, err)
regOtherUser, err := Client.RegisterTOTP(CTX, &user.RegisterTOTPRequest{
UserId: userVerified.GetUserId(),
})
require.NoError(t, err)
codeOtherUser, err := totp.GenerateCode(regOtherUser.Secret, time.Now())
require.NoError(t, err)
_, err = Client.VerifyTOTPRegistration(userVerifiedCtx, &user.VerifyTOTPRegistrationRequest{
UserId: userVerified.GetUserId(),
Code: codeOtherUser,
},
)
require.NoError(t, err)
type args struct {
ctx context.Context
req *user.RemoveTOTPRequest
}
tests := []struct {
name string
args args
want *user.RemoveTOTPResponse
wantErr bool
}{
{
name: "not added",
args: args{
ctx: Tester.WithAuthorizationToken(context.Background(), sessionToken),
req: &user.RemoveTOTPRequest{
UserId: userID,
},
},
wantErr: true,
},
{
name: "success",
args: args{
ctx: userVerifiedCtx,
req: &user.RemoveTOTPRequest{
UserId: userVerified.GetUserId(),
},
},
want: &user.RemoveTOTPResponse{
Details: &object.Details{
ResourceOwner: Tester.Organisation.ResourceOwner,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.RemoveTOTP(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, got)
integration.AssertDetails(t, tt.want, got)
})
}
}

View File

@ -590,7 +590,7 @@ func (s *Server) checkIntentToken(token string, intentID string) error {
}
func (s *Server) ListAuthenticationMethodTypes(ctx context.Context, req *user.ListAuthenticationMethodTypesRequest) (*user.ListAuthenticationMethodTypesResponse, error) {
authMethods, err := s.query.ListActiveUserAuthMethodTypes(ctx, req.GetUserId())
authMethods, err := s.query.ListUserAuthMethodTypes(ctx, req.GetUserId(), true)
if err != nil {
return nil, err
}

View File

@ -97,12 +97,7 @@ func (l *Login) resendPasswordSet(w http.ResponseWriter, r *http.Request, authRe
userID = authReq.UserID
authReqID = authReq.ID
}
passwordCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypePasswordResetCode, l.userCodeAlg)
if err != nil {
l.renderInitPassword(w, r, authReq, userID, "", err)
return
}
_, err = l.command.RequestSetPassword(setContext(r.Context(), userOrg), userID, userOrg, domain.NotificationTypeEmail, passwordCodeGenerator, authReqID)
_, err := l.command.RequestSetPassword(setContext(r.Context(), userOrg), userID, userOrg, domain.NotificationTypeEmail, authReqID)
l.renderInitPassword(w, r, authReq, userID, "", err)
}

View File

@ -1,10 +1,16 @@
package login
import (
"context"
"net/http"
"net/url"
"slices"
"github.com/zitadel/logging"
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
@ -16,15 +22,25 @@ const (
)
type mailVerificationFormData struct {
Code string `schema:"code"`
UserID string `schema:"userID"`
Resend bool `schema:"resend"`
Code string `schema:"code"`
UserID string `schema:"userID"`
Resend bool `schema:"resend"`
PasswordInit bool `schema:"passwordInit"`
Password string `schema:"password"`
PasswordConfirm string `schema:"passwordconfirm"`
}
type mailVerificationData struct {
baseData
profileData
UserID string
UserID string
Code string
PasswordInit bool
MinLength uint64
HasUppercase string
HasLowercase string
HasNumber string
HasSymbol string
}
func MailVerificationLink(origin, userID, code, orgID, authRequestID string) string {
@ -40,11 +56,32 @@ func (l *Login) handleMailVerification(w http.ResponseWriter, r *http.Request) {
authReq := l.checkOptionalAuthRequestOfEmailLinks(r)
userID := r.FormValue(queryUserID)
code := r.FormValue(queryCode)
if code != "" {
l.checkMailCode(w, r, authReq, userID, code)
if userID == "" && authReq == nil {
l.renderError(w, r, authReq, nil)
return
}
l.renderMailVerification(w, r, authReq, userID, nil)
if userID == "" {
userID = authReq.UserID
}
passwordInit := l.checkUserNoFirstFactor(r.Context(), userID)
if code != "" && !passwordInit {
l.checkMailCode(w, r, authReq, userID, code, "")
return
}
l.renderMailVerification(w, r, authReq, userID, code, passwordInit, nil)
}
func (l *Login) checkUserNoFirstFactor(ctx context.Context, userID string) bool {
authMethods, err := l.query.ListUserAuthMethodTypes(setUserContext(ctx, userID, ""), userID, false)
if err != nil {
logging.WithFields("userID", userID).OnError(err).Warn("unable to load user's auth methods for mail verification")
return false
}
return !slices.ContainsFunc(authMethods.AuthMethodTypes, func(m domain.UserAuthMethodType) bool {
return m == domain.UserAuthMethodTypeIDP ||
m == domain.UserAuthMethodTypePassword ||
m == domain.UserAuthMethodTypePasswordless
})
}
func (l *Login) handleMailVerificationCheck(w http.ResponseWriter, r *http.Request) {
@ -55,7 +92,12 @@ func (l *Login) handleMailVerificationCheck(w http.ResponseWriter, r *http.Reque
return
}
if !data.Resend {
l.checkMailCode(w, r, authReq, data.UserID, data.Code)
if data.PasswordInit && data.Password != data.PasswordConfirm {
err := zerrors.ThrowInvalidArgument(nil, "VIEW-fsdfd", "Errors.User.Password.ConfirmationWrong")
l.renderMailVerification(w, r, authReq, data.UserID, data.Code, data.PasswordInit, err)
return
}
l.checkMailCode(w, r, authReq, data.UserID, data.Code, data.Password)
return
}
var userOrg, authReqID string
@ -65,14 +107,14 @@ func (l *Login) handleMailVerificationCheck(w http.ResponseWriter, r *http.Reque
}
emailCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeVerifyEmailCode, l.userCodeAlg)
if err != nil {
l.checkMailCode(w, r, authReq, data.UserID, data.Code)
l.renderMailVerification(w, r, authReq, data.UserID, "", data.PasswordInit, err)
return
}
_, err = l.command.CreateHumanEmailVerificationCode(setContext(r.Context(), userOrg), data.UserID, userOrg, emailCodeGenerator, authReqID)
l.renderMailVerification(w, r, authReq, data.UserID, err)
l.renderMailVerification(w, r, authReq, data.UserID, "", data.PasswordInit, err)
}
func (l *Login) checkMailCode(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, userID, code string) {
func (l *Login) checkMailCode(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, userID, code, password string) {
userOrg := ""
if authReq != nil {
userID = authReq.UserID
@ -80,31 +122,52 @@ func (l *Login) checkMailCode(w http.ResponseWriter, r *http.Request, authReq *d
}
emailCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypeVerifyEmailCode, l.userCodeAlg)
if err != nil {
l.renderMailVerification(w, r, authReq, userID, err)
l.renderMailVerification(w, r, authReq, userID, "", password != "", err)
return
}
_, err = l.command.VerifyHumanEmail(setContext(r.Context(), userOrg), userID, code, userOrg, emailCodeGenerator)
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
_, err = l.command.VerifyHumanEmail(setContext(r.Context(), userOrg), userID, code, userOrg, password, userAgentID, emailCodeGenerator)
if err != nil {
l.renderMailVerification(w, r, authReq, userID, err)
l.renderMailVerification(w, r, authReq, userID, "", password != "", err)
return
}
l.renderMailVerified(w, r, authReq, userOrg)
}
func (l *Login) renderMailVerification(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, userID string, err error) {
func (l *Login) renderMailVerification(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, userID, code string, passwordInit bool, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
if userID == "" {
if userID == "" && authReq != nil {
userID = authReq.UserID
}
translator := l.getTranslator(r.Context(), authReq)
data := mailVerificationData{
baseData: l.getBaseData(r, authReq, translator, "EmailVerification.Title", "EmailVerification.Description", errID, errMessage),
UserID: userID,
profileData: l.getProfileData(authReq),
baseData: l.getBaseData(r, authReq, translator, "EmailVerification.Title", "EmailVerification.Description", errID, errMessage),
UserID: userID,
profileData: l.getProfileData(authReq),
Code: code,
PasswordInit: passwordInit,
}
if passwordInit {
policy := l.getPasswordComplexityPolicyByUserID(r, userID)
if policy != nil {
data.MinLength = policy.MinLength
if policy.HasUppercase {
data.HasUppercase = UpperCaseRegex
}
if policy.HasLowercase {
data.HasLowercase = LowerCaseRegex
}
if policy.HasSymbol {
data.HasSymbol = SymbolRegex
}
if policy.HasNumber {
data.HasNumber = NumberRegex
}
}
}
if authReq == nil {
user, err := l.query.GetUserByID(r.Context(), false, userID)

View File

@ -25,15 +25,7 @@ func (l *Login) handlePasswordReset(w http.ResponseWriter, r *http.Request) {
l.renderPasswordResetDone(w, r, authReq, err)
return
}
passwordCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypePasswordResetCode, l.userCodeAlg)
if err != nil {
if authReq.LoginPolicy.IgnoreUnknownUsernames && zerrors.IsNotFound(err) {
err = nil
}
l.renderPasswordResetDone(w, r, authReq, err)
return
}
_, err = l.command.RequestSetPassword(setContext(r.Context(), authReq.UserOrgID), user.ID, authReq.UserOrgID, domain.NotificationTypeEmail, passwordCodeGenerator, authReq.ID)
_, err = l.command.RequestSetPassword(setContext(r.Context(), authReq.UserOrgID), user.ID, authReq.UserOrgID, domain.NotificationTypeEmail, authReq.ID)
l.renderPasswordResetDone(w, r, authReq, err)
}

View File

@ -313,7 +313,7 @@ func (l *Login) chooseNextStep(w http.ResponseWriter, r *http.Request, authReq *
case *domain.ChangePasswordStep:
l.renderChangePassword(w, r, authReq, err)
case *domain.VerifyEMailStep:
l.renderMailVerification(w, r, authReq, "", err)
l.renderMailVerification(w, r, authReq, authReq.UserID, "", step.InitPassword, err)
case *domain.MFAPromptStep:
l.renderMFAPrompt(w, r, authReq, step, err)
case *domain.InitUserStep:

View File

@ -89,7 +89,7 @@ InitUserDone:
InitMFAPrompt:
Title: 两步验证设置
Description: 两步验证为您的账户提供了额外的安全保障。这确保只有你能访问你的账户。
Provider0: 软件应用(如 Google/Migrosoft Authenticator、Authy
Provider0: 软件应用(如 Google/Microsoft Authenticator、Authy
Provider1: 硬件设备(如 Face ID、Windows Hello、指纹
Provider3: 一次性密码短信
Provider4: 一次性密码电子邮件

View File

@ -17,13 +17,29 @@
<div class="fields">
<label class="lgn-label" for="code">{{t "EmailVerification.CodeLabel"}}</label>
<input class="lgn-input" type="text" id="code" name="code" autocomplete="off" autofocus required>
<input class="lgn-input" type="text" id="code" name="code" autocomplete="off" value="{{ .Code }}" {{if not .Code}}autofocus{{end}} required>
</div>
{{ if .PasswordInit }}
<div class="field">
<label class="lgn-label" for="password">{{t "InitUser.NewPasswordLabel"}}</label>
<input data-minlength="{{ .MinLength }}" data-has-uppercase="{{ .HasUppercase }}"
data-has-lowercase="{{ .HasLowercase }}" data-has-number="{{ .HasNumber }}"
data-has-symbol="{{ .HasSymbol }}" class="lgn-input" type="password" id="password" name="password"
autocomplete="new-password" autofocus required>
</div>
<div class="field">
<label class="lgn-label" for="passwordconfirm">{{t "InitUser.NewPasswordConfirm"}}</label>
<input class="lgn-input" type="password" id="passwordconfirm" name="passwordconfirm"
autocomplete="new-password" autofocus required>
{{ template "password-complexity-policy-description" . }}
</div>
{{ end }}
{{ template "error-message" .}}
<div class="lgn-actions lgn-reverse-order">
<button type="submit" id="submit-button" name="resend" value="false"
<button type="submit" id="{{if.PasswordInit}}init-button{{else}}submit-button{{end}}" name="resend" value="false"
class="lgn-primary lgn-raised-button">{{t "EmailVerification.NextButtonText"}}
</button>
@ -40,6 +56,11 @@
</div>
</form>
<script src="{{ resourceUrl "scripts/form_submit.js" }}"></script>
{{ if .PasswordInit }}
<script src="{{ resourceUrl "scripts/password_policy_check.js" }}"></script>
<script src="{{ resourceUrl "scripts/init_password_check.js" }}"></script>
{{ else }}
<script src="{{ resourceUrl "scripts/default_form_validation.js" }}"></script>
{{ end }}
{{template "main-bottom" .}}

View File

@ -52,6 +52,7 @@ type AuthRequestRepo struct {
ProjectProvider projectProvider
ApplicationProvider applicationProvider
CustomTextProvider customTextProvider
PasswordReset passwordReset
IdGenerator id.Generator
}
@ -96,6 +97,7 @@ type idpUserLinksProvider interface {
type userEventProvider interface {
UserEventsByID(ctx context.Context, id string, changeDate time.Time, eventTypes []eventstore.EventType) ([]eventstore.Event, error)
PasswordCodeExists(ctx context.Context, userID string) (exists bool, err error)
}
type userCommandProvider interface {
@ -125,6 +127,10 @@ type customTextProvider interface {
CustomTextListByTemplate(ctx context.Context, aggregateID string, text string, withOwnerRemoved bool) (texts *query.CustomTexts, err error)
}
type passwordReset interface {
RequestSetPassword(ctx context.Context, userID, resourceOwner string, notifyType domain.NotificationType, authRequestID string) (objectDetails *domain.ObjectDetails, err error)
}
func (repo *AuthRequestRepo) Health(ctx context.Context) error {
return repo.AuthRequests.Health(ctx)
}
@ -1046,7 +1052,7 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth
}
}
if isInternalLogin || (!isInternalLogin && len(request.LinkingUsers) > 0) {
step := repo.firstFactorChecked(request, user, userSession)
step := repo.firstFactorChecked(ctx, request, user, userSession)
if step != nil {
return append(steps, step), nil
}
@ -1065,7 +1071,9 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth
steps = append(steps, &domain.ChangePasswordStep{Expired: expired})
}
if !user.IsEmailVerified {
steps = append(steps, &domain.VerifyEMailStep{})
steps = append(steps, &domain.VerifyEMailStep{
InitPassword: !user.PasswordSet,
})
}
if user.UsernameChangeRequired {
steps = append(steps, &domain.ChangeUsernameStep{})
@ -1204,7 +1212,7 @@ func (repo *AuthRequestRepo) usersForUserSelection(ctx context.Context, request
return users, nil
}
func (repo *AuthRequestRepo) firstFactorChecked(request *domain.AuthRequest, user *user_model.UserView, userSession *user_model.UserSessionView) domain.NextStep {
func (repo *AuthRequestRepo) firstFactorChecked(ctx context.Context, request *domain.AuthRequest, user *user_model.UserView, userSession *user_model.UserSessionView) domain.NextStep {
if user.InitRequired {
return &domain.InitUserStep{PasswordSet: user.PasswordSet}
}
@ -1226,6 +1234,15 @@ func (repo *AuthRequestRepo) firstFactorChecked(request *domain.AuthRequest, use
}
if user.PasswordInitRequired {
if !user.IsEmailVerified {
return &domain.VerifyEMailStep{InitPassword: true}
}
exists, err := repo.UserEventProvider.PasswordCodeExists(ctx, user.ID)
logging.WithFields("userID", user.ID).OnError(err).Error("unable to check if password code exists")
if err == nil && !exists {
_, err = repo.PasswordReset.RequestSetPassword(ctx, user.ID, user.ResourceOwner, domain.NotificationTypeEmail, request.ID)
logging.WithFields("userID", user.ID).OnError(err).Error("unable to create password code")
}
return &domain.InitPasswordStep{}
}

View File

@ -108,7 +108,8 @@ func (m *mockViewNoUser) UserByID(string, string) (*user_view_model.UserView, er
}
type mockEventUser struct {
Event eventstore.Event
Event eventstore.Event
CodeExists bool
}
func (m *mockEventUser) UserEventsByID(ctx context.Context, id string, changeDate time.Time, types []eventstore.EventType) ([]eventstore.Event, error) {
@ -118,6 +119,10 @@ func (m *mockEventUser) UserEventsByID(ctx context.Context, id string, changeDat
return nil, nil
}
func (m *mockEventUser) PasswordCodeExists(ctx context.Context, userID string) (bool, error) {
return m.CodeExists, nil
}
func (m *mockEventUser) GetLatestUserSessionSequence(ctx context.Context, instanceID string) (*query.CurrentState, error) {
return &query.CurrentState{State: query.State{Sequence: 0}}, nil
}
@ -132,8 +137,8 @@ func (m *mockEventErrUser) UserEventsByID(ctx context.Context, id string, change
return nil, zerrors.ThrowInternal(nil, "id", "internal error")
}
func (m *mockEventErrUser) BulkAddExternalIDPs(ctx context.Context, userID string, externalIDPs []*user_model.ExternalIDP) error {
return zerrors.ThrowInternal(nil, "id", "internal error")
func (m *mockEventErrUser) PasswordCodeExists(ctx context.Context, userID string) (bool, error) {
return false, zerrors.ThrowInternal(nil, "id", "internal error")
}
type mockViewUser struct {
@ -298,6 +303,28 @@ func (m *mockIDPUserLinks) IDPUserLinks(ctx context.Context, queries *query.IDPU
return &query.IDPUserLinks{Links: m.idps}, nil
}
type mockPasswordReset struct {
t *testing.T
expectCall bool
}
func newMockPasswordReset(expectCall bool) func(*testing.T) passwordReset {
return func(t *testing.T) passwordReset {
return &mockPasswordReset{
t: t,
expectCall: expectCall,
}
}
}
func (m *mockPasswordReset) RequestSetPassword(ctx context.Context, userID, resourceOwner string, notifyType domain.NotificationType, authRequestID string) (objectDetails *domain.ObjectDetails, err error) {
if !m.expectCall {
m.t.Error("unexpected call to RequestSetPassword")
return nil, nil
}
return nil, err
}
func TestAuthRequestRepo_nextSteps(t *testing.T) {
type fields struct {
AuthRequests cache.AuthRequestCache
@ -316,6 +343,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
labelPolicyProvider labelPolicyProvider
passwordAgePolicyProvider passwordAgePolicyProvider
customTextProvider customTextProvider
passwordReset func(t *testing.T) passwordReset
}
type args struct {
request *domain.AuthRequest
@ -687,7 +715,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
fields{
userViewProvider: &mockViewUser{},
userEventProvider: &mockEventUser{
&es_models.Event{
Event: &es_models.Event{
AggregateType: user_repo.AggregateType,
Typ: user_repo.UserDeactivatedType,
},
@ -709,7 +737,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
fields{
userViewProvider: &mockViewUser{},
userEventProvider: &mockEventUser{
&es_models.Event{
Event: &es_models.Event{
AggregateType: user_repo.AggregateType,
Typ: user_repo.UserLockedType,
},
@ -929,7 +957,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
nil,
},
{
"password not set, init password step",
"password not set (email not verified), init password step",
fields{
userSessionViewProvider: &mockViewUserSession{},
userViewProvider: &mockViewUser{
@ -945,6 +973,54 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
idpUserLinksProvider: &mockIDPUserLinks{},
},
args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{}}, false},
[]domain.NextStep{&domain.VerifyEMailStep{InitPassword: true}},
nil,
},
{
"password not set (email verified), init password step",
fields{
userSessionViewProvider: &mockViewUserSession{},
userViewProvider: &mockViewUser{
PasswordInitRequired: true,
IsEmailVerified: true,
},
userEventProvider: &mockEventUser{
CodeExists: true,
},
lockoutPolicyProvider: &mockLockoutPolicy{
policy: &query.LockoutPolicy{
ShowFailures: true,
},
},
orgViewProvider: &mockViewOrg{State: domain.OrgStateActive},
idpUserLinksProvider: &mockIDPUserLinks{},
passwordReset: newMockPasswordReset(false),
},
args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{}}, false},
[]domain.NextStep{&domain.InitPasswordStep{}},
nil,
},
{
"password not set (email verified, password code not exists), create code, init password step",
fields{
userSessionViewProvider: &mockViewUserSession{},
userViewProvider: &mockViewUser{
PasswordInitRequired: true,
IsEmailVerified: true,
},
userEventProvider: &mockEventUser{
CodeExists: false,
},
lockoutPolicyProvider: &mockLockoutPolicy{
policy: &query.LockoutPolicy{
ShowFailures: true,
},
},
orgViewProvider: &mockViewOrg{State: domain.OrgStateActive},
idpUserLinksProvider: &mockIDPUserLinks{},
passwordReset: newMockPasswordReset(true),
},
args{&domain.AuthRequest{UserID: "UserID", LoginPolicy: &domain.LoginPolicy{}}, false},
[]domain.NextStep{&domain.InitPasswordStep{}},
nil,
},
@ -1720,6 +1796,9 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
PasswordAgePolicyProvider: tt.fields.passwordAgePolicyProvider,
CustomTextProvider: tt.fields.customTextProvider,
}
if tt.fields.passwordReset != nil {
repo.PasswordReset = tt.fields.passwordReset(t)
}
got, err := repo.nextSteps(context.Background(), tt.args.request, tt.args.checkLoggedIn)
if (err != nil && tt.wantErr == nil) || (tt.wantErr != nil && !tt.wantErr(err)) {
t.Errorf("nextSteps() wrong error = %v", err)
@ -2201,7 +2280,7 @@ func Test_userSessionByIDs(t *testing.T) {
agentID: "agentID",
user: &user_model.UserView{ID: "id", HumanView: &user_model.HumanView{FirstName: "FirstName"}},
eventProvider: &mockEventUser{
&es_models.Event{
Event: &es_models.Event{
AggregateType: user_repo.AggregateType,
Typ: user_repo.UserV1MFAOTPCheckSucceededType,
CreationDate: testNow,
@ -2224,7 +2303,7 @@ func Test_userSessionByIDs(t *testing.T) {
agentID: "agentID",
user: &user_model.UserView{ID: "id"},
eventProvider: &mockEventUser{
&es_models.Event{
Event: &es_models.Event{
AggregateType: user_repo.AggregateType,
Typ: user_repo.UserV1MFAOTPCheckSucceededType,
CreationDate: testNow,
@ -2251,7 +2330,7 @@ func Test_userSessionByIDs(t *testing.T) {
agentID: "agentID",
user: &user_model.UserView{ID: "id", HumanView: &user_model.HumanView{FirstName: "FirstName"}},
eventProvider: &mockEventUser{
&es_models.Event{
Event: &es_models.Event{
AggregateType: user_repo.AggregateType,
Typ: user_repo.UserV1MFAOTPCheckSucceededType,
CreationDate: testNow,
@ -2278,7 +2357,7 @@ func Test_userSessionByIDs(t *testing.T) {
agentID: "agentID",
user: &user_model.UserView{ID: "id"},
eventProvider: &mockEventUser{
&es_models.Event{
Event: &es_models.Event{
AggregateType: user_repo.AggregateType,
Typ: user_repo.UserRemovedType,
},
@ -2367,7 +2446,7 @@ func Test_userByID(t *testing.T) {
PasswordChangeRequired: true,
},
eventProvider: &mockEventUser{
&es_models.Event{
Event: &es_models.Event{
AggregateType: user_repo.AggregateType,
Typ: user_repo.UserV1PasswordChangedType,
CreationDate: testNow,

View File

@ -10,7 +10,9 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/repository/user"
usr_view "github.com/zitadel/zitadel/internal/user/repository/view"
"github.com/zitadel/zitadel/internal/zerrors"
)
type UserRepo struct {
@ -46,3 +48,40 @@ func (repo *UserRepo) UserEventsByID(ctx context.Context, id string, changeDate
}
return repo.Eventstore.Filter(ctx, query) //nolint:staticcheck
}
type passwordCodeCheck struct {
userID string
exists bool
events int
}
func (p *passwordCodeCheck) Reduce() error {
p.exists = p.events > 0
return nil
}
func (p *passwordCodeCheck) AppendEvents(events ...eventstore.Event) {
p.events += len(events)
}
func (p *passwordCodeCheck) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AddQuery().
AggregateTypes(user.AggregateType).
AggregateIDs(p.userID).
EventTypes(user.UserV1PasswordCodeAddedType, user.UserV1PasswordCodeSentType,
user.HumanPasswordCodeAddedType, user.HumanPasswordCodeSentType).
Builder()
}
func (repo *UserRepo) PasswordCodeExists(ctx context.Context, userID string) (exists bool, err error) {
model := &passwordCodeCheck{
userID: userID,
}
err = repo.Eventstore.FilterToQueryReducer(ctx, model)
if err != nil {
return false, zerrors.ThrowPermissionDenied(err, "EVENT-SJ642", "Errors.Internal")
}
return model.exists, nil
}

View File

@ -78,6 +78,7 @@ func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, c
ProjectProvider: queryView,
ApplicationProvider: queries,
CustomTextProvider: queries,
PasswordReset: command,
IdGenerator: id.SonyFlakeGenerator(),
},
eventstore.TokenRepo{

View File

@ -64,7 +64,7 @@ func (c *Commands) ChangeHumanEmail(ctx context.Context, email *domain.Email, em
return writeModelToEmail(existingEmail), nil
}
func (c *Commands) VerifyHumanEmail(ctx context.Context, userID, code, resourceowner string, emailCodeGenerator crypto.Generator) (*domain.ObjectDetails, error) {
func (c *Commands) VerifyHumanEmail(ctx context.Context, userID, code, resourceowner, optionalPassword, optionalUserAgentID string, emailCodeGenerator crypto.Generator) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-4M0ds", "Errors.User.UserIDMissing")
}
@ -82,21 +82,30 @@ func (c *Commands) VerifyHumanEmail(ctx context.Context, userID, code, resourceo
userAgg := UserAggregateFromWriteModel(&existingCode.WriteModel)
err = crypto.VerifyCode(existingCode.CodeCreationDate, existingCode.CodeExpiry, existingCode.Code, code, emailCodeGenerator.Alg())
if err == nil {
pushedEvents, err := c.eventstore.Push(ctx, user.NewHumanEmailVerifiedEvent(ctx, userAgg))
if err != nil {
return nil, err
}
err = AppendAndReduce(existingCode, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingCode.WriteModel), nil
if err != nil {
_, err = c.eventstore.Push(ctx, user.NewHumanEmailVerificationFailedEvent(ctx, userAgg))
logging.WithFields("userID", userAgg.ID).OnError(err).Error("NewHumanEmailVerificationFailedEvent push failed")
return nil, zerrors.ThrowInvalidArgument(err, "COMMAND-Gdsgs", "Errors.User.Code.Invalid")
}
_, err = c.eventstore.Push(ctx, user.NewHumanEmailVerificationFailedEvent(ctx, userAgg))
logging.LogWithFields("COMMAND-Dg2z5", "userID", userAgg.ID).OnError(err).Error("NewHumanEmailVerificationFailedEvent push failed")
return nil, zerrors.ThrowInvalidArgument(err, "COMMAND-Gdsgs", "Errors.User.Code.Invalid")
commands := []eventstore.Command{
user.NewHumanEmailVerifiedEvent(ctx, userAgg),
}
if optionalPassword != "" {
passwordCommand, err := c.setPasswordCommand(ctx, userAgg, domain.UserStateActive, optionalPassword, "", optionalUserAgentID, false, nil)
if err != nil {
return nil, err
}
commands = append(commands, passwordCommand)
}
pushedEvents, err := c.eventstore.Push(ctx, commands...)
if err != nil {
return nil, err
}
err = AppendAndReduce(existingCode, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&existingCode.WriteModel), nil
}
func (c *Commands) CreateHumanEmailVerificationCode(ctx context.Context, userID, resourceOwner string, emailCodeGenerator crypto.Generator, authRequestID string) (*domain.ObjectDetails, error) {

View File

@ -12,6 +12,7 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
)
@ -387,14 +388,17 @@ func TestCommandSide_ChangeHumanEmail(t *testing.T) {
func TestCommandSide_VerifyHumanEmail(t *testing.T) {
type fields struct {
eventstore func(*testing.T) *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
userPasswordHasher *crypto.Hasher
}
type args struct {
ctx context.Context
userID string
code string
resourceOwner string
secretGenerator crypto.Generator
ctx context.Context
userID string
code string
resourceOwner string
optionalUserAgentID string
optionalPassword string
secretGenerator crypto.Generator
}
type res struct {
want *domain.ObjectDetails
@ -587,13 +591,96 @@ func TestCommandSide_VerifyHumanEmail(t *testing.T) {
},
},
},
{
name: "valid code (with password and user agent), ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"nickname",
"displayname",
language.German,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
eventFromEventPusherWithCreationDateNow(
user.NewHumanEmailCodeAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("a"),
},
time.Hour*1,
"",
),
),
),
expectFilter(
eventFromEventPusher(
org.NewPasswordComplexityPolicyAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
1,
false,
false,
false,
false,
),
),
),
expectPush(
user.NewHumanEmailVerifiedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
),
user.NewHumanPasswordChangedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"$plain$x$password",
false,
"userAgentID",
),
),
),
userPasswordHasher: mockPasswordHasher("x"),
},
args: args{
ctx: context.Background(),
userID: "user1",
code: "a",
resourceOwner: "org1",
optionalPassword: "password",
optionalUserAgentID: "userAgentID",
secretGenerator: GetMockSecretGenerator(t),
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore(t),
eventstore: tt.fields.eventstore(t),
userPasswordHasher: tt.fields.userPasswordHasher,
}
got, err := r.VerifyHumanEmail(tt.args.ctx, tt.args.userID, tt.args.code, tt.args.resourceOwner, tt.args.secretGenerator)
got, err := r.VerifyHumanEmail(
tt.args.ctx,
tt.args.userID,
tt.args.code,
tt.args.resourceOwner,
tt.args.optionalPassword,
tt.args.optionalUserAgentID,
tt.args.secretGenerator,
)
if tt.res.err == nil {
assert.NoError(t, err)
}

View File

@ -239,6 +239,11 @@ func (c *Commands) HumanRemoveTOTP(ctx context.Context, userID, resourceOwner st
if existingOTP.State == domain.MFAStateUnspecified || existingOTP.State == domain.MFAStateRemoved {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-Hd9sd", "Errors.User.MFA.OTP.NotExisting")
}
if userID != authz.GetCtxData(ctx).UserID {
if err := c.checkPermission(ctx, domain.PermissionUserWrite, existingOTP.ResourceOwner, userID); err != nil {
return nil, err
}
}
userAgg := UserAggregateFromWriteModel(&existingOTP.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, user.NewHumanOTPRemovedEvent(ctx, userAgg))
if err != nil {

View File

@ -841,7 +841,8 @@ func TestCommands_HumanCheckMFATOTPSetup(t *testing.T) {
func TestCommandSide_RemoveHumanTOTP(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type (
args struct {
@ -891,7 +892,31 @@ func TestCommandSide_RemoveHumanTOTP(t *testing.T) {
},
},
{
name: "otp not existing, not found error",
name: "otp, no permission error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanOTPAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
nil,
),
),
),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
},
res: res{
err: zerrors.IsPermissionDenied,
},
},
{
name: "otp remove, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
@ -908,6 +933,7 @@ func TestCommandSide_RemoveHumanTOTP(t *testing.T) {
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
ctx: context.Background(),
@ -924,7 +950,8 @@ func TestCommandSide_RemoveHumanTOTP(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore(t),
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
got, err := r.HumanRemoveTOTP(tt.args.ctx, tt.args.userID, tt.args.orgID)
if tt.res.err == nil {

View File

@ -228,7 +228,7 @@ func (c *Commands) checkPasswordComplexity(ctx context.Context, newPassword stri
}
// RequestSetPassword generate and send out new code to change password for a specific user
func (c *Commands) RequestSetPassword(ctx context.Context, userID, resourceOwner string, notifyType domain.NotificationType, passwordVerificationCode crypto.Generator, authRequestID string) (objectDetails *domain.ObjectDetails, err error) {
func (c *Commands) RequestSetPassword(ctx context.Context, userID, resourceOwner string, notifyType domain.NotificationType, authRequestID string) (objectDetails *domain.ObjectDetails, err error) {
if userID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-M00oL", "Errors.User.UserIDMissing")
}
@ -244,11 +244,11 @@ func (c *Commands) RequestSetPassword(ctx context.Context, userID, resourceOwner
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-2M9sd", "Errors.User.NotInitialised")
}
userAgg := UserAggregateFromWriteModel(&existingHuman.WriteModel)
passwordCode, err := domain.NewPasswordCode(passwordVerificationCode)
passwordCode, err := c.newEncryptedCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypePasswordResetCode, c.userEncryption) //nolint:staticcheck
if err != nil {
return nil, err
}
pushedEvents, err := c.eventstore.Push(ctx, user.NewHumanPasswordCodeAddedEvent(ctx, userAgg, passwordCode.Code, passwordCode.Expiry, notifyType, authRequestID))
pushedEvents, err := c.eventstore.Push(ctx, user.NewHumanPasswordCodeAddedEvent(ctx, userAgg, passwordCode.Crypted, passwordCode.Expiry, notifyType, authRequestID))
if err != nil {
return nil, err
}

View File

@ -1111,14 +1111,14 @@ func TestCommandSide_ChangePassword(t *testing.T) {
func TestCommandSide_RequestSetPassword(t *testing.T) {
type fields struct {
eventstore func(*testing.T) *eventstore.Eventstore
newCode encrypedCodeFunc
}
type args struct {
ctx context.Context
userID string
resourceOwner string
notifyType domain.NotificationType
secretGenerator crypto.Generator
authRequestID string
ctx context.Context
userID string
resourceOwner string
notifyType domain.NotificationType
authRequestID string
}
type res struct {
want *domain.ObjectDetails
@ -1251,12 +1251,12 @@ func TestCommandSide_RequestSetPassword(t *testing.T) {
),
),
),
newCode: mockEncryptedCode("a", 1*time.Hour),
},
args: args{
ctx: context.Background(),
userID: "user1",
resourceOwner: "org1",
secretGenerator: GetMockSecretGenerator(t),
ctx: context.Background(),
userID: "user1",
resourceOwner: "org1",
},
res: res{
want: &domain.ObjectDetails{
@ -1307,13 +1307,13 @@ func TestCommandSide_RequestSetPassword(t *testing.T) {
),
),
),
newCode: mockEncryptedCode("a", 1*time.Hour),
},
args: args{
ctx: context.Background(),
userID: "user1",
resourceOwner: "org1",
secretGenerator: GetMockSecretGenerator(t),
authRequestID: "authRequestID",
ctx: context.Background(),
userID: "user1",
resourceOwner: "org1",
authRequestID: "authRequestID",
},
res: res{
want: &domain.ObjectDetails{
@ -1325,9 +1325,10 @@ func TestCommandSide_RequestSetPassword(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore(t),
eventstore: tt.fields.eventstore(t),
newEncryptedCode: tt.fields.newCode,
}
got, err := r.RequestSetPassword(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.notifyType, tt.args.secretGenerator, tt.args.authRequestID)
got, err := r.RequestSetPassword(tt.args.ctx, tt.args.userID, tt.args.resourceOwner, tt.args.notifyType, tt.args.authRequestID)
if tt.res.err == nil {
assert.NoError(t, err)
}

View File

@ -148,6 +148,52 @@ func TestCommandSide_AddMachine(t *testing.T) {
},
},
},
{
name: "add machine - custom id, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(),
expectFilter(
eventFromEventPusher(
org.NewDomainPolicyAddedEvent(context.Background(),
&user.NewAggregate("optionalID1", "org1").Aggregate,
true,
true,
true,
),
),
),
expectPush(
user.NewMachineAddedEvent(context.Background(),
&user.NewAggregate("optionalID1", "org1").Aggregate,
"username",
"name",
"description",
true,
domain.OIDCTokenTypeBearer,
),
),
),
},
args: args{
ctx: context.Background(),
machine: &Machine{
ObjectRoot: models.ObjectRoot{
AggregateID: "optionalID1",
ResourceOwner: "org1",
},
Description: "description",
Name: "name",
Username: "username",
},
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@ -319,7 +319,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) {
},
},
{
name: "add human (with initial code), ok",
name: "add human (email not verified, no password), ok (init code)",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
@ -389,7 +389,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) {
},
},
{
name: "add human (with password and initial code), ok",
name: "add human (email not verified, with password), ok (init code)",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
@ -459,6 +459,65 @@ func TestCommandSide_AddUserHuman(t *testing.T) {
wantID: "user1",
},
},
{
name: "add human (email not verified, no password, no allowInitMail), ok (email verification with passwordInit)",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
expectFilter(
eventFromEventPusher(
org.NewDomainPolicyAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
true,
true,
true,
),
),
),
expectPush(
newAddHumanEvent("", false, true, "", language.English),
user.NewHumanEmailCodeAddedEventV2(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("emailverify"),
},
1*time.Hour,
"",
false,
"",
),
),
),
checkPermission: newMockPermissionCheckAllowed(),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
newCode: mockEncryptedCode("emailverify", time.Hour),
},
args: args{
ctx: context.Background(),
orgID: "org1",
human: &AddHuman{
Username: "username",
FirstName: "firstname",
LastName: "lastname",
Email: Email{
Address: "email@test.ch",
},
PreferredLanguage: language.English,
},
secretGenerator: GetMockSecretGenerator(t),
allowInitMail: false,
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
res: res{
want: &domain.ObjectDetails{
ResourceOwner: "org1",
},
wantID: "user1",
},
},
{
name: "add human (with password and email code custom template), ok",
fields: fields{
@ -609,7 +668,7 @@ func TestCommandSide_AddUserHuman(t *testing.T) {
},
},
{
name: "add human email verified, ok",
name: "add human email verified and password, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
@ -1084,8 +1143,9 @@ func TestCommandSide_AddUserHuman(t *testing.T) {
},
wantID: "user1",
},
}, {
name: "add human (with return code), ok",
},
{
name: "add human (with phone return code), ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(),

View File

@ -140,6 +140,29 @@ func (c *Commands) verifyUserPhoneWithGenerator(ctx context.Context, userID, cod
return writeModelToObjectDetails(&cmd.model.WriteModel), nil
}
func (c *Commands) RemoveUserPhone(ctx context.Context, userID string) (*domain.ObjectDetails, error) {
return c.removeUserPhone(ctx, userID)
}
func (c *Commands) removeUserPhone(ctx context.Context, userID string) (*domain.ObjectDetails, error) {
cmd, err := c.NewUserPhoneEvents(ctx, userID)
if err != nil {
return nil, err
}
if authz.GetCtxData(ctx).UserID != userID {
if err = c.checkPermission(ctx, domain.PermissionUserWrite, cmd.aggregate.ResourceOwner, userID); err != nil {
return nil, err
}
}
if err = cmd.Remove(ctx); err != nil {
return nil, err
}
if _, err = cmd.Push(ctx); err != nil {
return nil, err
}
return writeModelToObjectDetails(&cmd.model.WriteModel), nil
}
// UserPhoneEvents allows step-by-step additions of events,
// operating on the Human Phone Model.
type UserPhoneEvents struct {
@ -191,6 +214,14 @@ func (c *UserPhoneEvents) Change(ctx context.Context, phone domain.PhoneNumber)
return nil
}
func (c *UserPhoneEvents) Remove(ctx context.Context) error {
if c.model.State == domain.PhoneStateRemoved || c.model.State == domain.PhoneStateUnspecified {
return zerrors.ThrowPreconditionFailed(nil, "COMMAND-ieJ2e", "Errors.User.Phone.NotFound")
}
c.events = append(c.events, user.NewHumanPhoneRemovedEvent(ctx, c.aggregate))
return nil
}
// SetVerified sets the phone number to verified.
func (c *UserPhoneEvents) SetVerified(ctx context.Context) {
c.events = append(c.events, user.NewHumanPhoneVerifiedEvent(ctx, c.aggregate))

Some files were not shown because too many files have changed in this diff Show More