Reorder
@@ -48,7 +48,7 @@
actions
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}"
color="warn"
- (click)="removeTarget(i)"
+ (click)="removeTarget(i, form)"
mat-icon-button
>
@@ -65,7 +65,7 @@
{{ 'ACTIONS.BACK' | translate }}
-
+
{{ 'ACTIONS.CONTINUE' | translate }}
diff --git a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.ts b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.ts
index e04368f8f4..60b4025650 100644
--- a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.ts
+++ b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-target/actions-two-add-action-target.component.ts
@@ -14,7 +14,7 @@ import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { MatButtonModule } from '@angular/material/button';
import { FormBuilder, FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
-import { ReplaySubject, switchMap } from 'rxjs';
+import { ObservedValueOf, ReplaySubject, shareReplay, switchMap } from 'rxjs';
import { MatRadioModule } from '@angular/material/radio';
import { ActionService } from 'src/app/services/action.service';
import { ToastService } from 'src/app/services/toast.service';
@@ -23,14 +23,13 @@ import { InputModule } from 'src/app/modules/input/input.module';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MessageInitShape } from '@bufbuild/protobuf';
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
-import { ExecutionTargetTypeSchema } from '@zitadel/proto/zitadel/action/v2beta/execution_pb';
import { MatSelectModule } from '@angular/material/select';
import { ActionConditionPipeModule } from 'src/app/pipes/action-condition-pipe/action-condition-pipe.module';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
-import { startWith } from 'rxjs/operators';
+import { map, startWith } from 'rxjs/operators';
import { TypeSafeCellDefModule } from 'src/app/directives/type-safe-cell-def/type-safe-cell-def.module';
import { CdkDrag, CdkDragDrop, CdkDropList, moveItemInArray } from '@angular/cdk/drag-drop';
-import { toObservable, toSignal } from '@angular/core/rxjs-interop';
+import { toSignal } from '@angular/core/rxjs-interop';
import { minArrayLengthValidator } from '../../../form-field/validators/validators';
import { ProjectRoleChipModule } from '../../../project-role-chip/project-role-chip.module';
import { MatTooltipModule } from '@angular/material/tooltip';
@@ -72,11 +71,12 @@ export class ActionsTwoAddActionTargetComponent {
}
@Output() public readonly back = new EventEmitter();
- @Output() public readonly continue = new EventEmitter[]>();
+ @Output() public readonly continue = new EventEmitter();
private readonly preselectedTargetIds$ = new ReplaySubject(1);
- protected readonly form: ReturnType;
+ protected readonly form$: ReturnType;
+
protected readonly targets: ReturnType;
private readonly selectedTargetIds: Signal;
protected readonly selectableTargets: Signal;
@@ -87,26 +87,27 @@ export class ActionsTwoAddActionTargetComponent {
private readonly actionService: ActionService,
private readonly toast: ToastService,
) {
- this.form = this.buildForm();
+ this.form$ = this.buildForm().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
this.targets = this.listTargets();
- this.selectedTargetIds = this.getSelectedTargetIds(this.form);
- this.selectableTargets = this.getSelectableTargets(this.targets, this.selectedTargetIds);
+ this.selectedTargetIds = this.getSelectedTargetIds(this.form$);
+ this.selectableTargets = this.getSelectableTargets(this.targets, this.selectedTargetIds, this.form$);
this.dataSource = this.getDataSource(this.targets, this.selectedTargetIds);
}
private buildForm() {
- const preselectedTargetIds = toSignal(this.preselectedTargetIds$, { initialValue: [] as string[] });
-
- return computed(() => {
- return this.fb.group({
- autocomplete: new FormControl('', { nonNullable: true }),
- selectedTargetIds: new FormControl(preselectedTargetIds(), {
- nonNullable: true,
- validators: [minArrayLengthValidator(1)],
- }),
- });
- });
+ return this.preselectedTargetIds$.pipe(
+ startWith([] as string[]),
+ map((preselectedTargetIds) => {
+ return this.fb.group({
+ autocomplete: new FormControl('', { nonNullable: true }),
+ selectedTargetIds: new FormControl(preselectedTargetIds, {
+ nonNullable: true,
+ validators: [minArrayLengthValidator(1)],
+ }),
+ });
+ }),
+ );
}
private listTargets() {
@@ -129,25 +130,35 @@ export class ActionsTwoAddActionTargetComponent {
return computed(targetsSignal);
}
- private getSelectedTargetIds(form: typeof this.form) {
- const selectedTargetIds$ = toObservable(form).pipe(
- startWith(form()),
- switchMap((form) => {
- const { selectedTargetIds } = form.controls;
+ private getSelectedTargetIds(form$: typeof this.form$) {
+ const selectedTargetIds$ = form$.pipe(
+ switchMap(({ controls: { selectedTargetIds } }) => {
return selectedTargetIds.valueChanges.pipe(startWith(selectedTargetIds.value));
}),
);
return toSignal(selectedTargetIds$, { requireSync: true });
}
- private getSelectableTargets(targets: typeof this.targets, selectedTargetIds: Signal) {
- return computed(() => {
+ private getSelectableTargets(targets: typeof this.targets, selectedTargetIds: Signal, form$: typeof this.form$) {
+ const autocomplete$ = form$.pipe(
+ switchMap(({ controls: { autocomplete } }) => {
+ return autocomplete.valueChanges.pipe(startWith(autocomplete.value));
+ }),
+ );
+ const autocompleteSignal = toSignal(autocomplete$, { requireSync: true });
+
+ const unselectedTargets = computed(() => {
const targetsCopy = new Map(targets().targets);
for (const selectedTargetId of selectedTargetIds()) {
targetsCopy.delete(selectedTargetId);
}
return Array.from(targetsCopy.values());
});
+
+ return computed(() => {
+ const autocomplete = autocompleteSignal().toLowerCase();
+ return unselectedTargets().filter(({ name }) => name.toLowerCase().includes(autocomplete));
+ });
}
private getDataSource(targetsSignal: typeof this.targets, selectedTargetIdsSignal: Signal) {
@@ -178,46 +189,39 @@ export class ActionsTwoAddActionTargetComponent {
return dataSource;
}
- protected addTarget(target: Target) {
- const { selectedTargetIds } = this.form().controls;
+ protected addTarget(target: Target, form: ObservedValueOf) {
+ const { selectedTargetIds } = form.controls;
selectedTargetIds.setValue([target.id, ...selectedTargetIds.value]);
- this.form().controls.autocomplete.setValue('');
+ form.controls.autocomplete.setValue('');
}
- protected removeTarget(index: number) {
- const { selectedTargetIds } = this.form().controls;
+ protected removeTarget(index: number, form: ObservedValueOf) {
+ const { selectedTargetIds } = form.controls;
const data = [...selectedTargetIds.value];
data.splice(index, 1);
selectedTargetIds.setValue(data);
}
- protected drop(event: CdkDragDrop) {
- const { selectedTargetIds } = this.form().controls;
+ protected drop(event: CdkDragDrop, form: ObservedValueOf) {
+ const { selectedTargetIds } = form.controls;
const data = [...selectedTargetIds.value];
moveItemInArray(data, event.previousIndex, event.currentIndex);
selectedTargetIds.setValue(data);
}
- protected handleEnter(event: Event) {
+ protected handleEnter(event: Event, form: ObservedValueOf) {
const selectableTargets = this.selectableTargets();
if (selectableTargets.length !== 1) {
return;
}
event.preventDefault();
- this.addTarget(selectableTargets[0]);
+ this.addTarget(selectableTargets[0], form);
}
protected submit() {
- const selectedTargets = this.selectedTargetIds().map((value) => ({
- type: {
- case: 'target' as const,
- value,
- },
- }));
-
- this.continue.emit(selectedTargets);
+ this.continue.emit(this.selectedTargetIds());
}
protected trackTarget(_: number, target: Target) {
diff --git a/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.html b/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.html
index 37d4f89dd0..717ee8f850 100644
--- a/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.html
+++ b/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.html
@@ -26,6 +26,9 @@
{{ 'ACTIONSTWO.TARGET.CREATE.TYPES.' + type | translate }}
+
+ {{ 'ACTIONSTWO.TARGET.CREATE.TYPES_DESCRIPTION' | translate }}
+
diff --git a/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.scss b/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.scss
index 34a7d5203d..a68e1e87cb 100644
--- a/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.scss
+++ b/console/src/app/modules/actions-two/actions-two-add-target/actions-two-add-target-dialog.component.scss
@@ -23,3 +23,7 @@
.name-hint {
font-size: 12px;
}
+
+.types-description {
+ white-space: pre-line;
+}
diff --git a/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets.component.html b/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets.component.html
index a6bde66e41..23b3b4bb89 100644
--- a/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets.component.html
+++ b/console/src/app/modules/actions-two/actions-two-targets/actions-two-targets.component.html
@@ -1,4 +1,7 @@
{{ 'ACTIONSTWO.TARGET.TITLE' | translate }}
+
+ {{ 'ACTIONSTWO.BETA_NOTE' | translate }}
+
{{ 'ACTIONSTWO.TARGET.DESCRIPTION' | translate }}
setTimeout(res, 1000));
@@ -86,4 +88,6 @@ export class ActionsTwoTargetsComponent {
this.toast.showError(error);
}
}
+
+ protected readonly InfoSectionType = InfoSectionType;
}
diff --git a/console/src/app/modules/actions-two/actions-two.module.ts b/console/src/app/modules/actions-two/actions-two.module.ts
index 45d70193f9..a940264eb2 100644
--- a/console/src/app/modules/actions-two/actions-two.module.ts
+++ b/console/src/app/modules/actions-two/actions-two.module.ts
@@ -20,6 +20,7 @@ import { ProjectRoleChipModule } from '../project-role-chip/project-role-chip.mo
import { ActionConditionPipeModule } from 'src/app/pipes/action-condition-pipe/action-condition-pipe.module';
import { MatSelectModule } from '@angular/material/select';
import { MatIconModule } from '@angular/material/icon';
+import { InfoSectionModule } from '../info-section/info-section.module';
@NgModule({
declarations: [
@@ -47,6 +48,7 @@ import { MatIconModule } from '@angular/material/icon';
TypeSafeCellDefModule,
ProjectRoleChipModule,
ActionConditionPipeModule,
+ InfoSectionModule,
],
exports: [ActionsTwoActionsComponent, ActionsTwoTargetsComponent, ActionsTwoTargetsTableComponent],
})
diff --git a/console/src/app/modules/settings-list/settings.ts b/console/src/app/modules/settings-list/settings.ts
index 79b92e2214..c96431fa30 100644
--- a/console/src/app/modules/settings-list/settings.ts
+++ b/console/src/app/modules/settings-list/settings.ts
@@ -231,6 +231,7 @@ export const ACTIONS: SidenavSetting = {
// todo: figure out roles
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
},
+ beta: true,
};
export const ACTIONS_TARGETS: SidenavSetting = {
@@ -241,4 +242,5 @@ export const ACTIONS_TARGETS: SidenavSetting = {
// todo: figure out roles
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
},
+ beta: true,
};
diff --git a/console/src/app/modules/sidenav/sidenav.component.html b/console/src/app/modules/sidenav/sidenav.component.html
index 277686852b..78e21e79f6 100644
--- a/console/src/app/modules/sidenav/sidenav.component.html
+++ b/console/src/app/modules/sidenav/sidenav.component.html
@@ -28,6 +28,7 @@
[attr.data-e2e]="'sidenav-element-' + setting.id"
>
{{ setting.i18nKey | translate }}
+ {{ 'SETTINGS.BETA' | translate }}
diff --git a/console/src/app/modules/sidenav/sidenav.component.scss b/console/src/app/modules/sidenav/sidenav.component.scss
index 383857751c..bb55a6999d 100644
--- a/console/src/app/modules/sidenav/sidenav.component.scss
+++ b/console/src/app/modules/sidenav/sidenav.component.scss
@@ -90,6 +90,10 @@
flex-shrink: 0;
}
+ .state {
+ margin-left: 0.5rem;
+ }
+
&:hover {
span {
opacity: 1;
diff --git a/console/src/app/modules/sidenav/sidenav.component.ts b/console/src/app/modules/sidenav/sidenav.component.ts
index 33539750a2..4b73491f08 100644
--- a/console/src/app/modules/sidenav/sidenav.component.ts
+++ b/console/src/app/modules/sidenav/sidenav.component.ts
@@ -11,6 +11,7 @@ export interface SidenavSetting {
[PolicyComponentServiceType.ADMIN]?: string[];
};
showWarn?: boolean;
+ beta?: boolean;
}
@Component({
diff --git a/console/src/app/pages/actions/actions.component.html b/console/src/app/pages/actions/actions.component.html
index 4c55308ced..af936e2351 100644
--- a/console/src/app/pages/actions/actions.component.html
+++ b/console/src/app/pages/actions/actions.component.html
@@ -6,6 +6,9 @@
info_outline
+
+ {{ 'DESCRIPTIONS.ACTIONS.ACTIONSTWO_NOTE' | translate }}
+
{{ 'DESCRIPTIONS.ACTIONS.DESCRIPTION' | translate }}
= new Subject();
+ protected maxActions: number | null = null;
+ protected ActionState = ActionState;
constructor(
private mgmtService: ManagementService,
breadcrumbService: BreadcrumbService,
private dialog: MatDialog,
private toast: ToastService,
+ destroyRef: DestroyRef,
) {
const bread: Breadcrumb = {
type: BreadcrumbType.ORG,
@@ -45,31 +45,24 @@ export class ActionsComponent implements OnDestroy {
};
breadcrumbService.setBreadcrumb([bread]);
- this.getFlowTypes();
+ this.getFlowTypes().then();
- this.typeControl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => {
+ this.typeControl.valueChanges.pipe(takeUntilDestroyed(destroyRef)).subscribe((value) => {
this.loadFlow((value as FlowType.AsObject).id);
});
}
- ngOnDestroy(): void {
- this.destroy$.next();
- this.destroy$.complete();
- }
-
- private getFlowTypes(): Promise {
- return this.mgmtService
- .listFlowTypes()
- .then((resp) => {
- this.typesForSelection = resp.resultList;
- if (!this.flow && resp.resultList[0]) {
- const type = resp.resultList[0];
- this.typeControl.setValue(type);
- }
- })
- .catch((error: any) => {
- this.toast.showError(error);
- });
+ private async getFlowTypes(): Promise {
+ try {
+ let resp = await this.mgmtService.listFlowTypes();
+ this.typesForSelection = resp.resultList;
+ if (!this.flow && resp.resultList[0]) {
+ const type = resp.resultList[0];
+ this.typeControl.setValue(type);
+ }
+ } catch (error) {
+ this.toast.showError(error);
+ }
}
private loadFlow(id: string) {
@@ -106,7 +99,7 @@ export class ActionsComponent implements OnDestroy {
});
}
- public openAddTrigger(flow: FlowType.AsObject, trigger?: TriggerType.AsObject): void {
+ protected openAddTrigger(flow: FlowType.AsObject, trigger?: TriggerType.AsObject): void {
const dialogRef = this.dialog.open(AddFlowDialogComponent, {
data: {
flowType: flow,
@@ -119,7 +112,7 @@ export class ActionsComponent implements OnDestroy {
if (req) {
this.mgmtService
.setTriggerActions(req.getActionIdsList(), req.getFlowType(), req.getTriggerType())
- .then((resp) => {
+ .then(() => {
this.toast.showInfo('FLOWS.FLOWCHANGED', true);
this.loadFlow(flow.id);
})
@@ -157,7 +150,7 @@ export class ActionsComponent implements OnDestroy {
}
}
- public removeTriggerActionsList(index: number) {
+ protected removeTriggerActionsList(index: number) {
if (this.flow.type && this.flow.triggerActionsList && this.flow.triggerActionsList[index]) {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
diff --git a/console/src/app/pages/instance/instance.component.ts b/console/src/app/pages/instance/instance.component.ts
index 546553132d..e52cdd7198 100644
--- a/console/src/app/pages/instance/instance.component.ts
+++ b/console/src/app/pages/instance/instance.component.ts
@@ -42,8 +42,6 @@ import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { EnvironmentService } from 'src/app/services/environment.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-import { NewFeatureService } from '../../services/new-feature.service';
-import { withLatestFromSynchronousFix } from '../../utils/withLatestFromSynchronousFix';
@Component({
selector: 'cnsl-instance',
templateUrl: './instance.component.html',
@@ -106,7 +104,6 @@ export class InstanceComponent {
private readonly envService: EnvironmentService,
activatedRoute: ActivatedRoute,
private readonly destroyRef: DestroyRef,
- private readonly featureService: NewFeatureService,
) {
this.loadMembers();
@@ -139,32 +136,7 @@ export class InstanceComponent {
}
private getSettingsList(): Observable {
- const features$ = this.getFeatures().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
-
- const actionsEnabled$ = features$.pipe(map((features) => features?.actions?.enabled));
-
- return this.authService
- .isAllowedMapper(this.defaultSettingsList, (setting) => setting.requiredRoles.admin || [])
- .pipe(
- withLatestFromSynchronousFix(actionsEnabled$),
- map(([settings, actionsEnabled]) =>
- settings
- .filter((setting) => actionsEnabled || setting.id !== ACTIONS.id)
- .filter((setting) => actionsEnabled || setting.id !== ACTIONS_TARGETS.id),
- ),
- );
- }
-
- private getFeatures() {
- return defer(() => this.featureService.getInstanceFeatures()).pipe(
- timeout(1000),
- catchError((error) => {
- if (!(error instanceof TimeoutError)) {
- this.toast.showError(error);
- }
- return of(undefined);
- }),
- );
+ return this.authService.isAllowedMapper(this.defaultSettingsList, (setting) => setting.requiredRoles.admin || []);
}
public loadMembers(): void {
diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json
index 4bd94d5ce6..30d9c53763 100644
--- a/console/src/assets/i18n/bg.json
+++ b/console/src/assets/i18n/bg.json
@@ -75,7 +75,8 @@
"FLOWS": {
"TITLE": "Потоци",
"DESCRIPTION": "Изберете поток за удостоверяване и активирайте вашето действие при конкретно събитие в този поток."
- }
+ },
+ "ACTIONSTWO_NOTE": "Actions V2, нова и подобрена версия на Actions, вече е налична. Настоящата версия все още е достъпна, но бъдещото развитие ще бъде фокусирано върху новата, която в крайна сметка ще замени текущата версия."
},
"SETTINGS": {
"INSTANCE": {
@@ -528,6 +529,7 @@
"APPLY": "Прилагам"
},
"ACTIONSTWO": {
+ "BETA_NOTE": "В момента използвате новата версия Actions V2, която е в бета фаза. Предишната версия 1 все още е достъпна, но ще бъде спряна в бъдеще. Моля, съобщавайте за всякакви проблеми или изпратете обратна връзка.",
"EXECUTION": {
"TITLE": "Действия",
"DESCRIPTION": "Действията ви позволяват да изпълнявате персонализиран код в отговор на API заявки, събития или специфични функции. Използвайте ги, за да разширите Zitadel, да автоматизирате работни процеси и да се интегрирате с други системи.",
@@ -618,6 +620,7 @@
"restCall": "REST извикване",
"restAsync": "REST асинхронно"
},
+ "TYPES_DESCRIPTION": "Webhook, обаждането обработва кода на състоянието, но отговорът е без значение\nCall, обаждането обработва кода на състоянието и отговора\nAsync, обаждането не обработва нито кода на състоянието, нито отговора, но може да бъде извикано паралелно с други цели",
"ENDPOINT": "Крайна точка",
"ENDPOINT_DESCRIPTION": "Въведете крайната точка, където се хоства вашият код. Уверете се, че е достъпна за нас!",
"TIMEOUT": "Време за изчакване",
@@ -1507,7 +1510,8 @@
"APPEARANCE": "Външен вид",
"OTHER": "други",
"STORAGE": "Съхранение"
- }
+ },
+ "BETA": "БЕТА"
},
"SETTING": {
"LANGUAGES": {
diff --git a/console/src/assets/i18n/cs.json b/console/src/assets/i18n/cs.json
index 6dd066789e..ee43e86822 100644
--- a/console/src/assets/i18n/cs.json
+++ b/console/src/assets/i18n/cs.json
@@ -75,7 +75,8 @@
"FLOWS": {
"TITLE": "Flows",
"DESCRIPTION": "Vyberte proces autentizace a spusťte vaši akci na konkrétní události v rámci tohoto procesu."
- }
+ },
+ "ACTIONSTWO_NOTE": "Actions V2, nová a vylepšená verze Actions, je nyní k dispozici. Aktuální verze je stále přístupná, ale budoucí vývoj se zaměří na novou verzi, která nakonec nahradí tu současnou."
},
"SETTINGS": {
"INSTANCE": {
@@ -529,6 +530,7 @@
"APPLY": "Platit"
},
"ACTIONSTWO": {
+ "BETA_NOTE": "Aktuálně používáte novou verzi Actions V2, která je v beta verzi. Předchozí verze 1 je stále k dispozici, ale v budoucnu bude ukončena. Prosím, hlaste jakékoliv problémy nebo zpětnou vazbu.",
"EXECUTION": {
"TITLE": "Akce",
"DESCRIPTION": "Akce vám umožňují spouštět vlastní kód v reakci na požadavky API, události nebo specifické funkce. Použijte je k rozšíření Zitadel, automatizaci pracovních postupů a integraci s dalšími systémy.",
@@ -619,6 +621,7 @@
"restCall": "REST Volání",
"restAsync": "REST Asynchronní"
},
+ "TYPES_DESCRIPTION": "Webhook, volání zpracovává stavový kód, ale odpověď je irelevantní\nCall, volání zpracovává stavový kód a odpověď\nAsync, volání nezpracovává ani stavový kód, ani odpověď, ale může být spuštěno paralelně s jinými cíli",
"ENDPOINT": "Koncový bod",
"ENDPOINT_DESCRIPTION": "Zadejte koncový bod, kde je hostován váš kód. Ujistěte se, že je pro nás přístupný!",
"TIMEOUT": "Časový limit",
@@ -1508,7 +1511,8 @@
"APPEARANCE": "Vzhled",
"OTHER": "Ostatní",
"STORAGE": "Data"
- }
+ },
+ "BETA": "BETA"
},
"SETTING": {
"LANGUAGES": {
diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json
index cb973006e4..3993674992 100644
--- a/console/src/assets/i18n/de.json
+++ b/console/src/assets/i18n/de.json
@@ -75,7 +75,8 @@
"FLOWS": {
"TITLE": "Flows",
"DESCRIPTION": "Wähle einen Authentifizierungsflow und löse deine Aktionen bei einem spezifischen Ereignis innerhalb dieses Flows aus."
- }
+ },
+ "ACTIONSTWO_NOTE": "Actions V2, eine neue und verbesserte Version von Actions, ist jetzt verfügbar. Die aktuelle Version ist weiterhin zugänglich, aber unsere zukünftige Entwicklung wird sich auf die neue Version konzentrieren, die schließlich die aktuelle ersetzen wird."
},
"SETTINGS": {
"INSTANCE": {
@@ -529,6 +530,7 @@
"APPLY": "Anwenden"
},
"ACTIONSTWO": {
+ "BETA_NOTE": "Sie verwenden derzeit die neuen Actions V2, die sich in der Beta-Phase befinden. Version 1 ist weiterhin verfügbar, wird jedoch in Zukunft eingestellt. Bitte melden Sie Probleme oder Feedback.",
"EXECUTION": {
"TITLE": "Aktionen",
"DESCRIPTION": "Aktionen ermöglichen es Ihnen, benutzerdefinierten Code als Reaktion auf API-Anfragen, Ereignisse oder bestimmte Funktionen auszuführen. Verwenden Sie sie, um Zitadel zu erweitern, Arbeitsabläufe zu automatisieren und sich in andere Systeme zu integrieren.",
@@ -619,6 +621,7 @@
"restCall": "REST Aufruf",
"restAsync": "REST Asynchron"
},
+ "TYPES_DESCRIPTION": "Webhook, der Aufruf verarbeitet den Statuscode, aber die Antwort ist irrelevant\nCall, der Aufruf verarbeitet den Statuscode und die Antwort\nAsync, der Aufruf verarbeitet weder Statuscode noch Antwort, kann aber parallel zu anderen Zielen aufgerufen werden",
"ENDPOINT": "Endpunkt",
"ENDPOINT_DESCRIPTION": "Geben Sie den Endpunkt ein, an dem Ihr Code gehostet wird. Stellen Sie sicher, dass er für uns zugänglich ist!",
"TIMEOUT": "Timeout",
@@ -1508,7 +1511,8 @@
"APPEARANCE": "Erscheinungsbild",
"OTHER": "Anderes",
"STORAGE": "Speicher"
- }
+ },
+ "BETA": "BETA"
},
"SETTING": {
"LANGUAGES": {
diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json
index be0a3d3f17..fd81bfd353 100644
--- a/console/src/assets/i18n/en.json
+++ b/console/src/assets/i18n/en.json
@@ -75,7 +75,8 @@
"FLOWS": {
"TITLE": "Flows",
"DESCRIPTION": "Choose an authentication flow and trigger your action on a specific event within this flow."
- }
+ },
+ "ACTIONSTWO_NOTE": "Actions V2 a new, improved version of Actions is now available. The current version is still accessible, but our future development will focus on the new one, which will eventually replace the current version."
},
"SETTINGS": {
"INSTANCE": {
@@ -529,6 +530,7 @@
"APPLY": "Apply"
},
"ACTIONSTWO": {
+ "BETA_NOTE": "You are currently using the new Actions V2, which is in beta. The previous Version 1 is still available but will be discontinued in the future. Please report any issues or feedback.",
"EXECUTION": {
"TITLE": "Actions",
"DESCRIPTION": "Actions let you run custom code in response to API requests, events or specific functions. Use them to extend Zitadel, automate workflows, and itegrate with other systems.",
@@ -619,6 +621,7 @@
"restCall": "REST Call",
"restAsync": "REST Async"
},
+ "TYPES_DESCRIPTION": "Webhook, the call handles the status code but response is irrelevant\nCall, the call handles the status code and response\nAsync, the call handles neither status code nor response, but can be called in parallel with other Targets",
"ENDPOINT": "Endpoint",
"ENDPOINT_DESCRIPTION": "Enter the endpoint where your code is hosted. Make sure it is accessible to us!",
"TIMEOUT": "Timeout",
@@ -1511,7 +1514,8 @@
"OTHER": "Other",
"STORAGE": "Storage",
"ACTIONS": "Actions"
- }
+ },
+ "BETA": "BETA"
},
"SETTING": {
"LANGUAGES": {
diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json
index 0fc95241af..aec024eacb 100644
--- a/console/src/assets/i18n/es.json
+++ b/console/src/assets/i18n/es.json
@@ -75,7 +75,8 @@
"FLOWS": {
"TITLE": "Flujos",
"DESCRIPTION": "Elige un flujo de autenticación y activa tu acción en un evento específico dentro de este flujo."
- }
+ },
+ "ACTIONSTWO_NOTE": "Actions V2, una nueva y mejorada versión de Actions, ya está disponible. La versión actual sigue siendo accesible, pero nuestro desarrollo futuro se centrará en la nueva, que acabará reemplazando la versión actual."
},
"SETTINGS": {
"INSTANCE": {
@@ -529,6 +530,7 @@
"APPLY": "Aplicar"
},
"ACTIONSTWO": {
+ "BETA_NOTE": "Actualmente estás usando la nueva versión Actions V2, que está en fase beta. La versión anterior 1 todavía está disponible, pero será descontinuada en el futuro. Por favor, informa de cualquier problema o comentario.",
"EXECUTION": {
"TITLE": "Acciones",
"DESCRIPTION": "Las acciones te permiten ejecutar código personalizado en respuesta a solicitudes de API, eventos o funciones específicas. Úsalas para extender Zitadel, automatizar flujos de trabajo e integrarte con otros sistemas.",
@@ -619,6 +621,7 @@
"restCall": "Llamada REST",
"restAsync": "REST Asíncrono"
},
+ "TYPES_DESCRIPTION": "Webhook, la llamada maneja el código de estado pero la respuesta es irrelevante\nCall, la llamada maneja el código de estado y la respuesta\nAsync, la llamada no maneja ni el código de estado ni la respuesta, pero puede ser llamada en paralelo con otros objetivos",
"ENDPOINT": "Punto de conexión",
"ENDPOINT_DESCRIPTION": "Introduce el punto de conexión donde se aloja tu código. ¡Asegúrate de que sea accesible para nosotros!",
"TIMEOUT": "Tiempo de espera",
@@ -1509,7 +1512,8 @@
"APPEARANCE": "Apariencia",
"OTHER": "Otros",
"STORAGE": "Datos"
- }
+ },
+ "BETA": "BETA"
},
"SETTING": {
"LANGUAGES": {
diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json
index 60a0a3e482..05e34ad846 100644
--- a/console/src/assets/i18n/fr.json
+++ b/console/src/assets/i18n/fr.json
@@ -75,7 +75,8 @@
"FLOWS": {
"TITLE": "Flux",
"DESCRIPTION": "Choisissez un flux d'authentification et déclenchez votre action sur un événement spécifique dans ce flux."
- }
+ },
+ "ACTIONSTWO_NOTE": "Actions V2, une nouvelle version améliorée de Actions, est désormais disponible. La version actuelle reste accessible, mais notre développement futur se concentrera sur la nouvelle, qui finira par remplacer la version actuelle."
},
"SETTINGS": {
"INSTANCE": {
@@ -529,6 +530,7 @@
"APPLY": "Appliquer"
},
"ACTIONSTWO": {
+ "BETA_NOTE": "Vous utilisez actuellement la nouvelle version Actions V2, qui est en phase bêta. L'ancienne version 1 est toujours disponible mais sera arrêtée à l'avenir. Veuillez signaler tout problème ou commentaire.",
"EXECUTION": {
"TITLE": "Actions",
"DESCRIPTION": "Les actions vous permettent d'exécuter du code personnalisé en réponse à des requêtes API, des événements ou des fonctions spécifiques. Utilisez-les pour étendre Zitadel, automatiser les flux de travail et vous intégrer à d'autres systèmes.",
@@ -619,6 +621,7 @@
"restCall": "Appel REST",
"restAsync": "REST Asynchrone"
},
+ "TYPES_DESCRIPTION": "Webhook, l'appel gère le code d'état mais la réponse est sans importance\nCall, l'appel gère le code d'état et la réponse\nAsync, l'appel ne gère ni le code d'état ni la réponse, mais peut être appelé en parallèle avec d'autres cibles",
"ENDPOINT": "Point de terminaison",
"ENDPOINT_DESCRIPTION": "Entrez le point de terminaison où votre code est hébergé. Assurez-vous qu'il nous est accessible !",
"TIMEOUT": "Délai d'attente",
@@ -1508,7 +1511,8 @@
"APPEARANCE": "Apparence",
"OTHER": "Autres",
"STORAGE": "Stockage"
- }
+ },
+ "BETA": "BÊTA"
},
"SETTING": {
"LANGUAGES": {
diff --git a/console/src/assets/i18n/hu.json b/console/src/assets/i18n/hu.json
index 5724c45a51..d46bc96153 100644
--- a/console/src/assets/i18n/hu.json
+++ b/console/src/assets/i18n/hu.json
@@ -75,7 +75,8 @@
"FLOWS": {
"TITLE": "Folyamatok",
"DESCRIPTION": "Válassz egy hitelesítési folyamatot, és váltasd ki a műveletedet egy adott esemény bekövetkezésekor ebben a folyamatban."
- }
+ },
+ "ACTIONSTWO_NOTE": "Az Actions V2, az Actions új, továbbfejlesztett verziója mostantól elérhető. A jelenlegi verzió továbbra is elérhető, de a jövőbeli fejlesztéseink az új verzióra összpontosítanak, amely végül felváltja a jelenlegi verziót."
},
"SETTINGS": {
"INSTANCE": {
@@ -529,6 +530,7 @@
"APPLY": "Alkalmaz"
},
"ACTIONSTWO": {
+ "BETA_NOTE": "Jelenleg az új Actions V2-t használja, amely béta verzióban van. Az előző 1-es verzió továbbra is elérhető, de a jövőben megszűnik. Kérjük, jelezze az esetleges problémákat vagy visszajelzéseit.",
"EXECUTION": {
"TITLE": "Műveletek",
"DESCRIPTION": "A műveletek lehetővé teszik egyedi kód futtatását API-kérésekre, eseményekre vagy konkrét függvényekre válaszul. Használja őket a Zitadel kiterjesztéséhez, a munkafolyamatok automatizálásához és más rendszerekkel való integrációhoz.",
@@ -619,6 +621,7 @@
"restCall": "REST Hívás",
"restAsync": "REST Aszinkron"
},
+ "TYPES_DESCRIPTION": "Webhook, a hívás kezeli az állapotkódot, de a válasz lényegtelen\nCall, a hívás kezeli az állapotkódot és a választ\nAsync, a hívás sem az állapotkódot, sem a választ nem kezeli, de párhuzamosan hívható más célokkal",
"ENDPOINT": "Végpont",
"ENDPOINT_DESCRIPTION": "Adja meg azt a végpontot, ahol a kódja található. Győződjön meg arról, hogy elérhető számunkra!",
"TIMEOUT": "Időtúllépés",
@@ -1508,7 +1511,8 @@
"APPEARANCE": "Megjelenés",
"OTHER": "Egyéb",
"STORAGE": "Tárolás"
- }
+ },
+ "BETA": "BÉTA"
},
"SETTING": {
"LANGUAGES": {
diff --git a/console/src/assets/i18n/id.json b/console/src/assets/i18n/id.json
index 77584e883b..f8831a224d 100644
--- a/console/src/assets/i18n/id.json
+++ b/console/src/assets/i18n/id.json
@@ -69,7 +69,8 @@
"FLOWS": {
"TITLE": "Mengalir",
"DESCRIPTION": "Pilih alur autentikasi dan picu tindakan Anda pada peristiwa tertentu dalam alur ini."
- }
+ },
+ "ACTIONSTWO_NOTE": "Actions V2, versi baru dan lebih baik dari Actions, sekarang tersedia. Versi saat ini masih dapat diakses, tetapi pengembangan di masa depan akan difokuskan pada versi baru ini yang pada akhirnya akan menggantikan versi saat ini."
},
"SETTINGS": {
"INSTANCE": {
@@ -496,6 +497,7 @@
"APPLY": "Menerapkan"
},
"ACTIONSTWO": {
+ "BETA_NOTE": "Anda saat ini menggunakan Actions V2 baru, yang masih dalam versi beta. Versi sebelumnya, Versi 1, masih tersedia tetapi akan dihentikan di masa depan. Silakan laporkan masalah atau berikan masukan.",
"EXECUTION": {
"TITLE": "Tindakan",
"DESCRIPTION": "Tindakan memungkinkan Anda menjalankan kode khusus sebagai respons terhadap permintaan API, peristiwa, atau fungsi tertentu. Gunakan ini untuk memperluas Zitadel, mengotomatiskan alur kerja, dan berintegrasi dengan sistem lain.",
@@ -586,6 +588,7 @@
"restCall": "Panggilan REST",
"restAsync": "REST Asinkron"
},
+ "TYPES_DESCRIPTION": "Webhook, panggilan menangani kode status tetapi respons tidak relevan\nCall, panggilan menangani kode status dan respons\nAsync, panggilan tidak menangani kode status maupun respons, tetapi dapat dipanggil secara paralel dengan Target lain",
"ENDPOINT": "Titik Akhir",
"ENDPOINT_DESCRIPTION": "Masukkan titik akhir tempat kode Anda dihosting. Pastikan dapat diakses oleh kami!",
"TIMEOUT": "Batas Waktu",
@@ -1386,7 +1389,8 @@
"APPEARANCE": "Penampilan",
"OTHER": "Lainnya",
"STORAGE": "Penyimpanan"
- }
+ },
+ "BETA": "BETA"
},
"SETTING": {
"LANGUAGES": {
diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json
index b01762b175..5dad683bca 100644
--- a/console/src/assets/i18n/it.json
+++ b/console/src/assets/i18n/it.json
@@ -75,7 +75,8 @@
"FLOWS": {
"TITLE": "Flussi",
"DESCRIPTION": "Scegli un flusso di autenticazione e attiva la tua azione su un evento specifico all'interno di questo flusso."
- }
+ },
+ "ACTIONSTWO_NOTE": "Actions V2, una nuova versione migliorata di Actions, è ora disponibile. La versione attuale è ancora accessibile, ma i futuri sviluppi si concentreranno su quella nuova, che alla fine sostituirà la versione corrente."
},
"SETTINGS": {
"INSTANCE": {
@@ -528,6 +529,7 @@
"APPLY": "Applicare"
},
"ACTIONSTWO": {
+ "BETA_NOTE": "Stai attualmente utilizzando la nuova versione Actions V2, che è in beta. La precedente Versione 1 è ancora disponibile, ma sarà dismessa in futuro. Ti preghiamo di segnalare eventuali problemi o feedback.",
"EXECUTION": {
"TITLE": "Azioni",
"DESCRIPTION": "Le azioni consentono di eseguire codice personalizzato in risposta a richieste API, eventi o funzioni specifiche. Usale per estendere Zitadel, automatizzare i flussi di lavoro e integrarti con altri sistemi.",
@@ -618,6 +620,7 @@
"restCall": "Chiamata REST",
"restAsync": "REST Asincrono"
},
+ "TYPES_DESCRIPTION": "Webhook, la chiamata gestisce il codice di stato ma la risposta è irrilevante\nCall, la chiamata gestisce il codice di stato e la risposta\nAsync, la chiamata non gestisce né il codice di stato né la risposta, ma può essere eseguita in parallelo con altri obiettivi",
"ENDPOINT": "Endpoint",
"ENDPOINT_DESCRIPTION": "Inserisci l'endpoint in cui è ospitato il tuo codice. Assicurati che sia accessibile per noi!",
"TIMEOUT": "Timeout",
@@ -1508,7 +1511,8 @@
"APPEARANCE": "Aspetto",
"OTHER": "Altro",
"STORAGE": "Dati"
- }
+ },
+ "BETA": "BETA"
},
"SETTING": {
"LANGUAGES": {
diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json
index c0429ec816..f09dfeb564 100644
--- a/console/src/assets/i18n/ja.json
+++ b/console/src/assets/i18n/ja.json
@@ -75,7 +75,8 @@
"FLOWS": {
"TITLE": "フロー",
"DESCRIPTION": "認証フローを選択し、そのフロー内の特定のイベントでアクションをトリガーします。"
- }
+ },
+ "ACTIONSTWO_NOTE": "Actions V2(アクションズV2)、改善された新しいバージョンが利用可能になりました。現在のバージョンも引き続き利用可能ですが、今後の開発は新バージョンに集中し、最終的には現在のバージョンを置き換える予定です。"
},
"SETTINGS": {
"INSTANCE": {
@@ -529,6 +530,7 @@
"APPLY": "アプライ"
},
"ACTIONSTWO": {
+ "BETA_NOTE": "現在、新しいActions V2(ベータ版)を使用しています。以前のバージョン1はまだ利用可能ですが、今後廃止される予定です。問題やフィードバックがあればお知らせください。",
"EXECUTION": {
"TITLE": "アクション",
"DESCRIPTION": "アクションを使用すると、APIリクエスト、イベント、または特定の関数に応答してカスタムコードを実行できます。これらを使用して、Zitadelを拡張し、ワークフローを自動化し、他のシステムと統合します。",
@@ -619,6 +621,7 @@
"restCall": "REST 呼び出し",
"restAsync": "REST 非同期"
},
+ "TYPES_DESCRIPTION": "Webhook、呼び出しはステータスコードを処理しますが、応答は無関係です\nCall、呼び出しはステータスコードと応答を処理します\nAsync、呼び出しはステータスコードも応答も処理しませんが、他のターゲットと並行して呼び出すことができます",
"ENDPOINT": "エンドポイント",
"ENDPOINT_DESCRIPTION": "コードがホストされているエンドポイントを入力します。アクセス可能であることを確認してください。",
"TIMEOUT": "タイムアウト",
@@ -1508,7 +1511,8 @@
"APPEARANCE": "設定",
"OTHER": "その他",
"STORAGE": "ストレージ"
- }
+ },
+ "BETA": "ベータ"
},
"SETTING": {
"LANGUAGES": {
diff --git a/console/src/assets/i18n/ko.json b/console/src/assets/i18n/ko.json
index b791234cd5..304f52e127 100644
--- a/console/src/assets/i18n/ko.json
+++ b/console/src/assets/i18n/ko.json
@@ -75,7 +75,8 @@
"FLOWS": {
"TITLE": "플로우",
"DESCRIPTION": "인증 플로우를 선택하고 이 플로우 내의 특정 이벤트에서 작업을 트리거하세요."
- }
+ },
+ "ACTIONSTWO_NOTE": "Actions V2, 개선된 새로운 버전이 출시되었습니다. 현재 버전은 여전히 접근할 수 있지만, 앞으로의 개발은 새로운 버전에 집중될 것이며, 결국 현재 버전을 대체할 것입니다."
},
"SETTINGS": {
"INSTANCE": {
@@ -529,6 +530,7 @@
"APPLY": "적용"
},
"ACTIONSTWO": {
+ "BETA_NOTE": "현재 베타 버전인 새로운 Actions V2를 사용하고 있습니다. 이전 버전 1은 여전히 사용 가능하지만, 향후 중단될 예정입니다. 문제나 피드백이 있으면 알려주세요.",
"EXECUTION": {
"TITLE": "작업",
"DESCRIPTION": "작업을 통해 API 요청, 이벤트 또는 특정 함수에 대한 응답으로 사용자 지정 코드를 실행할 수 있습니다. 이를 사용하여 Zitadel을 확장하고 워크플로를 자동화하며 다른 시스템과 통합합니다.",
@@ -619,6 +621,7 @@
"restCall": "REST 호출",
"restAsync": "REST 비동기"
},
+ "TYPES_DESCRIPTION": "Webhook, 호출은 상태 코드를 처리하지만 응답은 중요하지 않습니다\nCall, 호출은 상태 코드와 응답을 처리합니다\nAsync, 호출은 상태 코드나 응답을 처리하지 않지만 다른 대상과 병렬로 호출할 수 있습니다",
"ENDPOINT": "엔드포인트",
"ENDPOINT_DESCRIPTION": "코드가 호스팅되는 엔드포인트를 입력하십시오. 우리에게 액세스할 수 있는지 확인하십시오!",
"TIMEOUT": "시간 초과",
@@ -1508,7 +1511,8 @@
"APPEARANCE": "외형",
"OTHER": "기타",
"STORAGE": "저장소"
- }
+ },
+ "BETA": "베타"
},
"SETTING": {
"LANGUAGES": {
diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json
index 22e0e6d3d7..1ab4dce534 100644
--- a/console/src/assets/i18n/mk.json
+++ b/console/src/assets/i18n/mk.json
@@ -75,7 +75,8 @@
"FLOWS": {
"TITLE": "Текови",
"DESCRIPTION": "Изберете тек на автентификација и активирајте ја вашата акција на специфичен настан во тој тек."
- }
+ },
+ "ACTIONSTWO_NOTE": "Actions V2, нова и подобрена верзија на Actions, сега е достапна. Сегашната верзија сè уште е достапна, но идниот развој ќе биде насочен кон новата верзија, која на крајот ќе ја замени сегашната."
},
"SETTINGS": {
"INSTANCE": {
@@ -529,6 +530,7 @@
"APPLY": "Пријавете се"
},
"ACTIONSTWO": {
+ "BETA_NOTE": "",
"EXECUTION": {
"TITLE": "Акции",
"DESCRIPTION": "Акциите ви овозможуваат да извршувате прилагоден код како одговор на API барања, настани или специфични функции. Користете ги за да го проширите Zitadel, да ги автоматизирате работните процеси и да се интегрирате со други системи.",
@@ -619,6 +621,7 @@
"restCall": "REST Повик",
"restAsync": "REST Асинхроно"
},
+ "TYPES_DESCRIPTION": "Webhook, повикот го обработува статусниот код но одговорот е ирелевантен\nCall, повикот го обработува статусниот код и одговорот\nAsync, повикот не го обработува ниту статусниот код ниту одговорот, но може да се повика паралелно со други цели",
"ENDPOINT": "Крајна точка",
"ENDPOINT_DESCRIPTION": "Внесете ја крајната точка каде што е хостиран вашиот код. Осигурете се дека е достапна за нас!",
"TIMEOUT": "Време на истекување",
@@ -1509,7 +1512,8 @@
"APPEARANCE": "Изглед",
"OTHER": "Друго",
"STORAGE": "складирање"
- }
+ },
+ "BETA": "БЕТА"
},
"SETTING": {
"LANGUAGES": {
diff --git a/console/src/assets/i18n/nl.json b/console/src/assets/i18n/nl.json
index 112474e770..a08f62ed20 100644
--- a/console/src/assets/i18n/nl.json
+++ b/console/src/assets/i18n/nl.json
@@ -75,7 +75,8 @@
"FLOWS": {
"TITLE": "Stromen",
"DESCRIPTION": "Kies een authenticatiestroom en activeer je actie bij een specifieke gebeurtenis binnen deze stroom."
- }
+ },
+ "ACTIONSTWO_NOTE": "Actions V2, een nieuwe en verbeterde versie van Actions, is nu beschikbaar. De huidige versie blijft toegankelijk, maar onze toekomstige ontwikkeling zal zich richten op de nieuwe versie, die uiteindelijk de huidige zal vervangen."
},
"SETTINGS": {
"INSTANCE": {
@@ -529,6 +530,7 @@
"APPLY": "Toepassen"
},
"ACTIONSTWO": {
+ "BETA_NOTE": "U gebruikt momenteel de nieuwe Actions V2, die zich in de bètaversie bevindt. De vorige versie 1 is nog beschikbaar maar zal in de toekomst worden stopgezet. Meld alstublieft eventuele problemen of feedback.",
"EXECUTION": {
"TITLE": "Acties",
"DESCRIPTION": "Met acties kunt u aangepaste code uitvoeren als reactie op API-verzoeken, gebeurtenissen of specifieke functies. Gebruik ze om Zitadel uit te breiden, workflows te automatiseren en te integreren met andere systemen.",
@@ -619,6 +621,7 @@
"restCall": "REST Aanroep",
"restAsync": "REST Asynchroon"
},
+ "TYPES_DESCRIPTION": "Webhook, de oproep verwerkt de statuscode maar de reactie is irrelevant\nCall, de oproep verwerkt de statuscode en de reactie\nAsync, de oproep verwerkt noch de statuscode noch de reactie, maar kan parallel aan andere doelen worden aangeroepen",
"ENDPOINT": "Eindpunt",
"ENDPOINT_DESCRIPTION": "Voer het eindpunt in waar uw code wordt gehost. Zorg ervoor dat het voor ons toegankelijk is!",
"TIMEOUT": "Time-out",
@@ -1508,7 +1511,8 @@
"APPEARANCE": "Verschijning",
"OTHER": "Andere",
"STORAGE": "opslag"
- }
+ },
+ "BETA": "BÈTA"
},
"SETTING": {
"LANGUAGES": {
diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json
index 3244ccb4a6..3902378af6 100644
--- a/console/src/assets/i18n/pl.json
+++ b/console/src/assets/i18n/pl.json
@@ -75,7 +75,8 @@
"FLOWS": {
"TITLE": "Przepływy",
"DESCRIPTION": "Wybierz przepływ uwierzytelniania i wywołaj swoją akcję przy określonym zdarzeniu w tym przepływie."
- }
+ },
+ "ACTIONSTWO_NOTE": "Actions V2, nowa, ulepszona wersja Actions, jest już dostępna. Obecna wersja jest nadal dostępna, ale przyszły rozwój będzie skoncentrowany na nowej wersji, która ostatecznie zastąpi obecną."
},
"SETTINGS": {
"INSTANCE": {
@@ -528,6 +529,7 @@
"APPLY": "Stosować"
},
"ACTIONSTWO": {
+ "BETA_NOTE": "Obecnie korzystasz z nowej wersji Actions V2, która jest w fazie beta. Poprzednia wersja 1 jest nadal dostępna, ale w przyszłości zostanie wycofana. Prosimy o zgłaszanie wszelkich problemów lub opinii.",
"EXECUTION": {
"TITLE": "Akcje",
"DESCRIPTION": "Akcje umożliwiają uruchamianie niestandardowego kodu w odpowiedzi na żądania API, zdarzenia lub określone funkcje. Użyj ich, aby rozszerzyć Zitadel, zautomatyzować przepływy pracy i zintegrować się z innymi systemami.",
@@ -618,6 +620,7 @@
"restCall": "Wywołanie REST",
"restAsync": "REST Asynchroniczny"
},
+ "TYPES_DESCRIPTION": "Webhook, wywołanie obsługuje kod stanu, ale odpowiedź jest nieistotna\nCall, wywołanie obsługuje kod stanu i odpowiedź\nAsync, wywołanie nie obsługuje ani kodu stanu, ani odpowiedzi, ale może być wywoływane równolegle z innymi celami",
"ENDPOINT": "Punkt końcowy",
"ENDPOINT_DESCRIPTION": "Wprowadź punkt końcowy, w którym hostowany jest Twój kod. Upewnij się, że jest dla nas dostępny!",
"TIMEOUT": "Limit czasu",
@@ -1507,7 +1510,8 @@
"APPEARANCE": "Wygląd",
"OTHER": "Inne",
"STORAGE": "składowanie"
- }
+ },
+ "BETA": "BETA"
},
"SETTING": {
"LANGUAGES": {
diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json
index 30b0f1d4e8..016785f2c8 100644
--- a/console/src/assets/i18n/pt.json
+++ b/console/src/assets/i18n/pt.json
@@ -75,7 +75,8 @@
"FLOWS": {
"TITLE": "Fluxos",
"DESCRIPTION": "Escolha um fluxo de autenticação e acione sua ação em um evento específico dentro desse fluxo."
- }
+ },
+ "ACTIONSTWO_NOTE": "Actions V2, uma nova e melhorada versão de Actions, já está disponível. A versão atual ainda é acessível, mas o nosso desenvolvimento futuro se concentrará na nova versão, que acabará por substituir a atual."
},
"SETTINGS": {
"INSTANCE": {
@@ -529,6 +530,7 @@
"APPLY": "Aplicar"
},
"ACTIONSTWO": {
+ "BETA_NOTE": "Você está atualmente usando a nova Actions V2, que está em versão beta. A versão anterior 1 ainda está disponível, mas será descontinuada no futuro. Por favor, reporte quaisquer problemas ou envie feedback.",
"EXECUTION": {
"TITLE": "Ações",
"DESCRIPTION": "As ações permitem que você execute código personalizado em resposta a solicitações de API, eventos ou funções específicas. Use-as para estender o Zitadel, automatizar fluxos de trabalho e integrar-se a outros sistemas.",
@@ -619,6 +621,7 @@
"restCall": "Chamada REST",
"restAsync": "REST Assíncrono"
},
+ "TYPES_DESCRIPTION": "Webhook, a chamada lida com o código de status, mas a resposta é irrelevante\nCall, a chamada lida com o código de status e a resposta\nAsync, a chamada não lida nem com o código de status nem com a resposta, mas pode ser chamada em paralelo com outros alvos",
"ENDPOINT": "Ponto de Extremidade",
"ENDPOINT_DESCRIPTION": "Insira o ponto de extremidade onde seu código está hospedado. Certifique-se de que ele esteja acessível para nós!",
"TIMEOUT": "Tempo Limite",
@@ -1509,7 +1512,8 @@
"APPEARANCE": "Aparência",
"OTHER": "Outro",
"STORAGE": "armazenar"
- }
+ },
+ "BETA": "BETA"
},
"SETTING": {
"LANGUAGES": {
diff --git a/console/src/assets/i18n/ro.json b/console/src/assets/i18n/ro.json
index 6c73852beb..4ad511c466 100644
--- a/console/src/assets/i18n/ro.json
+++ b/console/src/assets/i18n/ro.json
@@ -75,7 +75,8 @@
"FLOWS": {
"TITLE": "Fluxuri",
"DESCRIPTION": "Alegeți un flux de autentificare și declanșați acțiunea dvs. la un anumit eveniment din cadrul acestui flux."
- }
+ },
+ "ACTIONSTWO_NOTE": "Actions V2, o nouă versiune îmbunătățită a Actions, este acum disponibilă. Versiunea actuală este încă accesibilă, dar dezvoltarea viitoare se va concentra pe cea nouă, care în cele din urmă va înlocui versiunea actuală."
},
"SETTINGS": {
"INSTANCE": {
@@ -529,6 +530,7 @@
"APPLY": "Aplicați"
},
"ACTIONSTWO": {
+ "BETA_NOTE": "În prezent utilizați noua versiune Actions V2, care este în faza beta. Versiunea anterioară 1 este încă disponibilă, dar va fi întreruptă în viitor. Vă rugăm să raportați orice problemă sau feedback.",
"EXECUTION": {
"TITLE": "Acțiuni",
"DESCRIPTION": "Acțiunile vă permit să rulați cod personalizat ca răspuns la cereri API, evenimente sau funcții specifice. Folosiți-le pentru a extinde Zitadel, a automatiza fluxurile de lucru și a vă integra cu alte sisteme.",
@@ -619,6 +621,7 @@
"restCall": "Apel REST",
"restAsync": "REST Asincron"
},
+ "TYPES_DESCRIPTION": "Webhook, apelul gestionează codul de stare, dar răspunsul este irelevant\nCall, apelul gestionează codul de stare și răspunsul\nAsync, apelul nu gestionează nici codul de stare, nici răspunsul, dar poate fi apelat în paralel cu alte Ținte",
"ENDPOINT": "Punct Final",
"ENDPOINT_DESCRIPTION": "Introduceți punctul final unde este găzduit codul dvs. Asigurați-vă că este accesibil pentru noi!",
"TIMEOUT": "Timeout",
@@ -1506,7 +1509,8 @@
"APPEARANCE": "Aspect",
"OTHER": "Altele",
"STORAGE": "Stocare"
- }
+ },
+ "BETA": "BETA"
},
"SETTING": {
"LANGUAGES": {
diff --git a/console/src/assets/i18n/ru.json b/console/src/assets/i18n/ru.json
index 6e2d7df7b4..43bc266be0 100644
--- a/console/src/assets/i18n/ru.json
+++ b/console/src/assets/i18n/ru.json
@@ -75,7 +75,8 @@
"FLOWS": {
"TITLE": "Потоки",
"DESCRIPTION": "Выберите поток аутентификации и активируйте ваше действие на определенном событии в этом потоке."
- }
+ },
+ "ACTIONSTWO_NOTE": "Actions V2, новая и улучшенная версия Actions, теперь доступна. Текущая версия всё ещё доступна, но дальнейшая разработка будет сосредоточена на новой версии, которая в конечном итоге заменит текущую."
},
"SETTINGS": {
"INSTANCE": {
@@ -529,6 +530,7 @@
"APPLY": "Применять"
},
"ACTIONSTWO": {
+ "BETA_NOTE": "Вы используете новую версию Actions V2, которая находится в бета-тестировании. Предыдущая версия 1 всё ещё доступна, но будет отключена в будущем. Пожалуйста, сообщайте о любых проблемах или отправляйте отзывы.",
"EXECUTION": {
"TITLE": "Действия",
"DESCRIPTION": "Действия позволяют запускать пользовательский код в ответ на API-запросы, события или определенные функции. Используйте их для расширения Zitadel, автоматизации рабочих процессов и интеграции с другими системами.",
@@ -619,6 +621,7 @@
"restCall": "REST Вызов",
"restAsync": "REST Асинхронный"
},
+ "TYPES_DESCRIPTION": "Webhook, вызов обрабатывает код состояния, но ответ не имеет значения\nCall, вызов обрабатывает код состояния и ответ\nAsync, вызов не обрабатывает ни код состояния, ни ответ, но может выполняться параллельно с другими целями",
"ENDPOINT": "Конечная точка",
"ENDPOINT_DESCRIPTION": "Введите конечную точку, где размещен ваш код. Убедитесь, что он доступен для нас!",
"TIMEOUT": "Тайм-аут",
@@ -1553,7 +1556,8 @@
"APPEARANCE": "Вид",
"OTHER": "Другое",
"STORAGE": "хранилище"
- }
+ },
+ "BETA": "БЕТА"
},
"SETTING": {
"LANGUAGES": {
diff --git a/console/src/assets/i18n/sv.json b/console/src/assets/i18n/sv.json
index e747571f7a..00b7854603 100644
--- a/console/src/assets/i18n/sv.json
+++ b/console/src/assets/i18n/sv.json
@@ -75,7 +75,8 @@
"FLOWS": {
"TITLE": "Flöden",
"DESCRIPTION": "Välj ett autentiseringsflöde och trigga din åtgärd vid en specifik händelse inom detta flöde."
- }
+ },
+ "ACTIONSTWO_NOTE": "Actions V2, en ny och förbättrad version av Actions, är nu tillgänglig. Den nuvarande versionen är fortfarande tillgänglig, men framtida utveckling kommer att fokusera på den nya, som så småningom kommer att ersätta den nuvarande."
},
"SETTINGS": {
"INSTANCE": {
@@ -529,6 +530,7 @@
"APPLY": "Tillämpa"
},
"ACTIONSTWO": {
+ "BETA_NOTE": "Du använder för närvarande nya Actions V2, som är i betaversion. Den tidigare versionen 1 är fortfarande tillgänglig men kommer att avvecklas i framtiden. Vänligen rapportera eventuella problem eller ge feedback.",
"EXECUTION": {
"TITLE": "Åtgärder",
"DESCRIPTION": "Åtgärder låter dig köra anpassad kod som svar på API-förfrågningar, händelser eller specifika funktioner. Använd dem för att utöka Zitadel, automatisera arbetsflöden och integrera med andra system.",
@@ -619,6 +621,7 @@
"restCall": "REST Anrop",
"restAsync": "REST Asynkron"
},
+ "TYPES_DESCRIPTION": "Webhook, anropet hanterar statuskoden men svaret är irrelevant\nCall, anropet hanterar statuskoden och svaret\nAsync, anropet hanterar varken statuskod eller svar men kan anropas parallellt med andra mål",
"ENDPOINT": "Slutpunkt",
"ENDPOINT_DESCRIPTION": "Ange slutpunkten där din kod finns. Se till att den är tillgänglig för oss!",
"TIMEOUT": "Tidsgräns",
@@ -1512,7 +1515,8 @@
"APPEARANCE": "Utseende",
"OTHER": "Övrigt",
"STORAGE": "Lagring"
- }
+ },
+ "BETA": "BETA"
},
"SETTING": {
"LANGUAGES": {
diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json
index 945e4200ef..496b3d528e 100644
--- a/console/src/assets/i18n/zh.json
+++ b/console/src/assets/i18n/zh.json
@@ -75,7 +75,8 @@
"FLOWS": {
"TITLE": "流程",
"DESCRIPTION": "选择一个认证流程,并在该流程中的特定事件上触发您的操作。"
- }
+ },
+ "ACTIONSTWO_NOTE": "Actions V2,一个全新改进版的Actions,现在已上线。目前版本仍可使用,但未来开发将专注于新版本,最终将取代当前版本。"
},
"SETTINGS": {
"INSTANCE": {
@@ -529,6 +530,7 @@
"APPLY": "申请"
},
"ACTIONSTWO": {
+ "BETA_NOTE": "您目前正在使用新的 Actions V2(测试版)。之前的版本1仍可使用,但未来将停止支持。请报告任何问题或反馈意见。",
"EXECUTION": {
"TITLE": "操作",
"DESCRIPTION": "操作允许您运行自定义代码以响应 API 请求、事件或特定函数。使用它们来扩展 Zitadel、自动化工作流程并与其他系统集成。",
@@ -619,6 +621,7 @@
"restCall": "REST 调用",
"restAsync": "REST 异步"
},
+ "TYPES_DESCRIPTION": "Webhook,调用处理状态码但响应无关紧要\nCall,调用处理状态码和响应\nAsync,调用既不处理状态码也不处理响应,但可以与其他目标并行调用",
"ENDPOINT": "端点",
"ENDPOINT_DESCRIPTION": "输入您的代码托管的端点。确保我们可以访问它!",
"TIMEOUT": "超时",
@@ -1508,7 +1511,8 @@
"APPEARANCE": "外观",
"OTHER": "其他",
"STORAGE": "贮存"
- }
+ },
+ "BETA": "测试版"
},
"SETTING": {
"LANGUAGES": {
diff --git a/console/yarn.lock b/console/yarn.lock
index 5dd602dfa8..2e586abedb 100644
--- a/console/yarn.lock
+++ b/console/yarn.lock
@@ -3452,29 +3452,22 @@
js-yaml "^3.10.0"
tslib "^2.4.0"
-"@zitadel/client@^1.0.7":
- version "1.0.7"
- resolved "https://registry.yarnpkg.com/@zitadel/client/-/client-1.0.7.tgz#39dc8d3d10bfa01e5cf56205ba188f79c39f052d"
- integrity sha512-sZG4NEa8vQBt3+4W1AesY+5DstDBuZiqGH2EM+UqbO5D93dlDZInXqZ5oRE7RSl2Bk5ED9mbMFrB7b8DuRw72A==
+"@zitadel/client@1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@zitadel/client/-/client-1.2.0.tgz#8cdc3090f75fcf3a78c4f0266d3c56a0cca6821a"
+ integrity sha512-Q20nXhKD7VDb8D1UxhDxubC70GFrSPckrJviPR/rAfRR5slUIRTk3AvDS6Q1WvUn4Xtt+btnq52Z5O8lZtVG0w==
dependencies:
"@bufbuild/protobuf" "^2.2.2"
"@connectrpc/connect" "^2.0.0"
"@connectrpc/connect-node" "^2.0.0"
"@connectrpc/connect-web" "^2.0.0"
- "@zitadel/proto" "1.0.4"
+ "@zitadel/proto" "1.2.0"
jose "^5.3.0"
-"@zitadel/proto@1.0.4":
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/@zitadel/proto/-/proto-1.0.4.tgz#e2fe9895f2960643c3619191255aa2f4913ad873"
- integrity sha512-s13ZMhuOTe0b+geV+JgJud+kpYdq7TgkuCe7RIY+q4Xs5KC0FHMKfvbAk/jpFbD+TSQHiwo/TBNZlGHdwUR9Ig==
- dependencies:
- "@bufbuild/protobuf" "^2.2.2"
-
-"@zitadel/proto@1.0.5-sha-4118a9d":
- version "1.0.5-sha-4118a9d"
- resolved "https://registry.yarnpkg.com/@zitadel/proto/-/proto-1.0.5-sha-4118a9d.tgz#e09025f31b2992b061d5416a0d1e12ef370118cc"
- integrity sha512-7ZFwISL7TqdCkfEUx7/H6UJDqX8ZP2jqG1ulbELvEQ2smrK365Zs7AkJGeB/xbVdhQW9BOhWy2R+Jni7sfxd2w==
+"@zitadel/proto@1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@zitadel/proto/-/proto-1.2.0.tgz#9b9a40defcd9e8464627cc99ac3fd7bcf8994ffd"
+ integrity sha512-OqHgyCnD9l950xswdVNPIsLA01qSpOPf+0bYqYJWHafytIBbvGNJRnypu4X0LnaFXLM6LakkP4pWYeiGLmwxaw==
dependencies:
"@bufbuild/protobuf" "^2.2.2"
From c36b0ab2e2a0a2632cffae4fe6ac086a126bc2a8 Mon Sep 17 00:00:00 2001
From: Elio Bischof
Date: Tue, 29 Apr 2025 16:12:34 +0200
Subject: [PATCH 025/181] docs(self-hosting): add login to lb example (#9496)
# Which Problems Are Solved
We have no docs for self-hosting the login using the standard login as a
standalone docker container.
# How the Problems Are Solved
A common self-hosting case is to publish the login at the same domain as
Zitadel behind a reverse proxy.
That's why we extend the load balancing example.
We refocus the example from *making TLS work* to *running multiple
services behind the proxy and connect them using an internal network and
DNS*. I decided this together with @fforootd.
For authenticating with the login application, we have to set up a
service user and give it the role IAM_LOGIN_CLIENT. We do so in the
use-new-login "job" container as `zitadel setup` only supports Zitadel
users with the role IAM_ADMIN AFAIR.
The login application relies on a healthy Zitadel API on startup, which
is why we fix the containers readiness reports.
# Additional Changes
- We deploy the init and setup jobs independently, because this better
reflects our production recommendatinons.
It gives more control over the upgrade process.
- We use the ExternalDomain *127.0.0.1.sslip.io* instead of *my.domain*,
because this doesn't require changing the local DNS resolution by
changing */etc/hosts* for local tests.
# Testing
The commands in the preview docs use to the configuration files on main.
This is fine when the PR is merged but not for testing the PR.
Replace the used links to make them point to the PRs changed files.
Instead of the commands in the preview docs, use these:
```bash
# Download the docker compose example configuration.
wget https://raw.githubusercontent.com/zitadel/zitadel/refs/heads/docs-compose-login/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml
# Download the Traefik example configuration.
wget https://raw.githubusercontent.com/zitadel/zitadel/refs/heads/docs-compose-login/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml
# Download and adjust the example configuration file containing standard configuration.
wget https://raw.githubusercontent.com/zitadel/zitadel/refs/heads/docs-compose-login/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml
# Download and adjust the example configuration file containing secret configuration.
wget https://raw.githubusercontent.com/zitadel/zitadel/refs/heads/docs-compose-login/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-secrets.yaml
# Download and adjust the example configuration file containing database initialization configuration.
wget https://raw.githubusercontent.com/zitadel/zitadel/refs/heads/docs-compose-login/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml
# A single ZITADEL instance always needs the same 32 bytes long masterkey
# Generate one to a file if you haven't done so already and pass it as environment variable
LC_ALL=C tr -dc '[:graph:]' ./zitadel-masterkey
export ZITADEL_MASTERKEY="$(cat ./zitadel-masterkey)"
# Run the database and application containers
docker compose up --detach --wait
```
# Additional Context
- Closes https://github.com/zitadel/DevOps/issues/111
- Depends on https://github.com/zitadel/typescript/pull/412
- Contributes to road map item
https://github.com/zitadel/zitadel/issues/9481
---
.../deploy/loadbalancing-example/.gitignore | 1 +
.../loadbalancing-example/docker-compose.yaml | 153 +++++++++++++++---
.../example-traefik.yaml | 57 ++-----
.../example-zitadel-config.yaml | 31 ++--
.../example-zitadel-init-steps.yaml | 12 +-
.../loadbalancing-example.mdx | 35 ++--
6 files changed, 183 insertions(+), 106 deletions(-)
create mode 100644 docs/docs/self-hosting/deploy/loadbalancing-example/.gitignore
diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/.gitignore b/docs/docs/self-hosting/deploy/loadbalancing-example/.gitignore
new file mode 100644
index 0000000000..bd98bacd66
--- /dev/null
+++ b/docs/docs/self-hosting/deploy/loadbalancing-example/.gitignore
@@ -0,0 +1 @@
+.env-file
diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml b/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml
index d1d8c95bb2..013fc2aa22 100644
--- a/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml
+++ b/docs/docs/self-hosting/deploy/loadbalancing-example/docker-compose.yaml
@@ -1,48 +1,157 @@
services:
- traefik:
+ db:
+ image: postgres:17-alpine
+ restart: unless-stopped
+ environment:
+ - POSTGRES_USER=root
+ - POSTGRES_PASSWORD=postgres
networks:
- - 'zitadel'
- image: "traefik:latest"
- ports:
- - "80:80"
- - "443:443"
+ - 'storage'
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready", "-d", "db_prod"]
+ interval: 10s
+ timeout: 60s
+ retries: 5
+ start_period: 10s
volumes:
- - "./example-traefik.yaml:/etc/traefik/traefik.yaml"
+ - 'data:/var/lib/postgresql/data:rw'
- zitadel:
- restart: 'always'
+ zitadel-init:
+ restart: 'no'
networks:
- - 'zitadel'
+ - 'storage'
image: 'ghcr.io/zitadel/zitadel:latest'
- command: 'start-from-init --config /example-zitadel-config.yaml --config /example-zitadel-secrets.yaml --steps /example-zitadel-init-steps.yaml --masterkey "${ZITADEL_MASTERKEY}" --tlsMode external'
+ command: 'init --config /example-zitadel-config.yaml --config /example-zitadel-secrets.yaml'
depends_on:
db:
condition: 'service_healthy'
volumes:
- './example-zitadel-config.yaml:/example-zitadel-config.yaml:ro'
- './example-zitadel-secrets.yaml:/example-zitadel-secrets.yaml:ro'
+
+ zitadel-setup:
+ restart: 'no'
+ networks:
+ - 'storage'
+ # We use the debug image so we have the environment to
+ # - create the .env file for the login to authenticate at Zitadel
+ # - set the correct permissions for the .env-file folder
+ image: 'ghcr.io/zitadel/zitadel:latest-debug'
+ user: root
+ entrypoint: '/bin/sh'
+ command:
+ - -c
+ - >
+ /app/zitadel setup
+ --config /example-zitadel-config.yaml
+ --config /example-zitadel-secrets.yaml
+ --steps /example-zitadel-init-steps.yaml
+ --masterkey ${ZITADEL_MASTERKEY} &&
+ mv /pat /.env-file/pat || exit 0 &&
+ echo ZITADEL_SERVICE_USER_TOKEN=$(cat /.env-file/pat) > /.env-file/.env &&
+ chown -R 1001:${GID} /.env-file &&
+ chmod -R 770 /.env-file
+ environment:
+ - GID
+ depends_on:
+ zitadel-init:
+ condition: 'service_completed_successfully'
+ restart: false
+ volumes:
+ - './.env-file:/.env-file:rw'
+ - './example-zitadel-config.yaml:/example-zitadel-config.yaml:ro'
+ - './example-zitadel-secrets.yaml:/example-zitadel-secrets.yaml:ro'
- './example-zitadel-init-steps.yaml:/example-zitadel-init-steps.yaml:ro'
- db:
- image: postgres:17-alpine
- restart: always
- environment:
- - POSTGRES_USER=root
- - POSTGRES_PASSWORD=postgres
+ zitadel:
+ restart: 'unless-stopped'
networks:
- - 'zitadel'
+ - 'backend'
+ - 'storage'
+ image: 'ghcr.io/zitadel/zitadel:latest'
+ command: >
+ start --config /example-zitadel-config.yaml
+ --config /example-zitadel-secrets.yaml
+ --masterkey ${ZITADEL_MASTERKEY}
+ depends_on:
+ zitadel-setup:
+ condition: 'service_completed_successfully'
+ restart: true
+ volumes:
+ - './example-zitadel-config.yaml:/example-zitadel-config.yaml:ro'
+ - './example-zitadel-secrets.yaml:/example-zitadel-secrets.yaml:ro'
+ ports:
+ - "8080:8080"
healthcheck:
- test: ["CMD-SHELL", "pg_isready", "-d", "db_prod"]
+ test: [
+ "CMD", "/app/zitadel", "ready",
+ "--config", "/example-zitadel-config.yaml",
+ "--config", "/example-zitadel-secrets.yaml"
+ ]
interval: 10s
timeout: 60s
retries: 5
- start_period: 10s
+ start_period: 10s
+
+ # The use-new-login service configures Zitadel to use the new login v2 for all applications.
+ # It also gives the setupped machine user the necessary IAM_LOGIN_CLIENT role.
+ use-new-login:
+ restart: 'on-failure'
+ user: "1001"
+ networks:
+ - 'backend'
+ image: 'badouralix/curl-jq:alpine'
+ entrypoint: '/bin/sh'
+ command:
+ - -c
+ - >
+ curl -X PUT -H "Host: 127.0.0.1.sslip.io" -H "Authorization: Bearer $(cat ./.env-file/pat)" --insecure http://zitadel:8080/v2/features/instance -d '{"loginV2": {"required": true}}' &&
+ LOGIN_USER=$(curl --fail-with-body -H "Host: 127.0.0.1.sslip.io" -H "Authorization: Bearer $(cat ./.env-file/pat)" --insecure http://zitadel:8080/auth/v1/users/me | jq -r '.user.id') &&
+ curl -X PUT -H "Host: 127.0.0.1.sslip.io" -H "Authorization: Bearer $(cat ./.env-file/pat)" --insecure http://zitadel:8080/admin/v1/members/$${LOGIN_USER} -d '{"roles": ["IAM_OWNER", "IAM_LOGIN_CLIENT"]}'
volumes:
- - 'data:/var/lib/postgresql/data:rw'
+ - './.env-file:/.env-file:ro'
+ depends_on:
+ zitadel:
+ condition: 'service_healthy'
+ restart: false
+
+ login:
+ restart: 'unless-stopped'
+ networks:
+ - 'backend'
+ image: 'ghcr.io/zitadel/login:main'
+ environment:
+ - ZITADEL_API_URL=http://zitadel:8080
+ - CUSTOM_REQUEST_HEADERS=Host:127.0.0.1.sslip.io
+ - NEXT_PUBLIC_BASE_PATH="/ui/v2/login"
+ user: "${UID:-1000}"
+ volumes:
+ - './.env-file:/.env-file:ro'
+ depends_on:
+ zitadel:
+ condition: 'service_healthy'
+ restart: false
+
+ traefik:
+ restart: 'unless-stopped'
+ networks:
+ - 'backend'
+ image: "traefik:latest"
+ ports:
+ - "80:80"
+ - "443:443"
+ volumes:
+ - "./example-traefik.yaml:/etc/traefik/traefik.yaml"
+ depends_on:
+ zitadel:
+ condition: 'service_healthy'
+ login:
+ condition: 'service_started'
networks:
- zitadel:
+ storage:
+ backend:
volumes:
data:
diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml b/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml
index c16f74a46d..a3af425172 100644
--- a/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml
+++ b/docs/docs/self-hosting/deploy/loadbalancing-example/example-traefik.yaml
@@ -4,66 +4,37 @@ log:
accessLog: {}
entrypoints:
- web:
- address: ":80"
-
websecure:
address: ":443"
-tls:
- stores:
- default:
- # generates self-signed certificates
- defaultCertificate:
-
providers:
file:
filename: /etc/traefik/traefik.yaml
http:
- middlewares:
- zitadel:
- headers:
- isDevelopment: false
- allowedHosts:
- - 'my.domain'
- customRequestHeaders:
- authority: 'my.domain'
- redirect-to-https:
- redirectScheme:
- scheme: https
- port: 443
- permanent: true
-
routers:
- # Redirect HTTP to HTTPS
- router0:
+ login:
entryPoints:
- - web
- middlewares:
- - redirect-to-https
- rule: 'HostRegexp(`my.domain`, `{subdomain:[a-z]+}.my.domain`)'
- service: zitadel
- # The actual ZITADEL router
- router1:
+ - websecure
+ service: login
+ rule: 'Host(`127.0.0.1.sslip.io`) && PathPrefix(`/ui/v2/login`)'
+ tls: {}
+ zitadel:
entryPoints:
- websecure
service: zitadel
- middlewares:
- - zitadel
- rule: 'HostRegexp(`my.domain`, `{subdomain:[a-z]+}.my.domain`)'
- tls:
- domains:
- - main: "my.domain"
- sans:
- - "*.my.domain"
- - "my.domain"
+ rule: 'Host(`127.0.0.1.sslip.io`) && !PathPrefix(`/ui/v2/login`)'
+ tls: {}
- # Add the service
services:
+ login:
+ loadBalancer:
+ servers:
+ - url: http://login:3000
+ passHostHeader: true
zitadel:
loadBalancer:
servers:
- # h2c is the scheme for unencrypted HTTP/2
- url: h2c://zitadel:8080
passHostHeader: true
+
diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml b/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml
index 392bf1148e..fadd39373d 100644
--- a/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml
+++ b/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-config.yaml
@@ -1,26 +1,29 @@
# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/defaults.yaml
-Log:
- Level: 'info'
-# Make ZITADEL accessible over HTTPs, not HTTP
ExternalSecure: true
-ExternalDomain: my.domain
+ExternalDomain: 127.0.0.1.sslip.io
ExternalPort: 443
+# Traefik terminates TLS. Inside the Docker network, we use plain text.
+TLS.Enabled: false
+
# If not using the docker compose example, adjust these values for connecting ZITADEL to your PostgreSQL
Database:
postgres:
Host: 'db'
Port: 5432
Database: zitadel
- User:
- SSL:
- Mode: 'disable'
- Admin:
- SSL:
- Mode: 'disable'
+ User.SSL.Mode: 'disable'
+ Admin.SSL.Mode: 'disable'
-LogStore:
- Access:
- Stdout:
- Enabled: true
+# By default, ZITADEL should redirect to /ui/v2/login
+OIDC:
+ DefaultLoginURLV2: "/ui/v2/login/login?authRequest=" # ZITADEL_OIDC_DEFAULTLOGINURLV2
+ DefaultLogoutURLV2: "/ui/v2/login/logout?post_logout_redirect=" # ZITADEL_OIDC_DEFAULTLOGOUTURLV2
+SAML.DefaultLoginURLV2: "/ui/v2/login/login?authRequest=" # ZITADEL_SAML_DEFAULTLOGINURLV2
+
+# Access logs allow us to debug Network issues
+LogStore.Access.Stdout.Enabled: true
+
+# Skipping the MFA init step allows us to immediately authenticate at the console
+DefaultInstance.LoginPolicy.MfaInitSkipLifetime: "0s"
diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml b/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml
index 804e3d18d8..be63164ced 100644
--- a/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml
+++ b/docs/docs/self-hosting/deploy/loadbalancing-example/example-zitadel-init-steps.yaml
@@ -1,8 +1,12 @@
# All possible options and their defaults: https://github.com/zitadel/zitadel/blob/main/cmd/setup/steps.yaml
FirstInstance:
+ PatPath: '/pat'
Org:
- Name: 'My Org'
+ # We want to authenticate immediately at the console without changing the password
Human:
- # use the loginname root@my-org.my.domain
- Username: 'root'
- Password: 'RootPassword1!'
+ PasswordChangeRequired: false
+ Machine:
+ Machine:
+ Username: 'login-container'
+ Name: 'Login Container'
+ Pat.ExpirationDate: '2029-01-01T00:00:00Z'
diff --git a/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx b/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx
index d5e3984568..88cd4c7700 100644
--- a/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx
+++ b/docs/docs/self-hosting/deploy/loadbalancing-example/loadbalancing-example.mdx
@@ -1,5 +1,5 @@
---
-title: A ZITADEL Load Balancing Example
+title: A Zitadel Load Balancing Example
---
import CodeBlock from '@theme/CodeBlock';
@@ -8,16 +8,16 @@ import ExampleTraefikSource from '!!raw-loader!./example-traefik.yaml'
import ExampleZITADELConfigSource from '!!raw-loader!./example-zitadel-config.yaml'
import ExampleZITADELSecretsSource from '!!raw-loader!./example-zitadel-secrets.yaml'
import ExampleZITADELInitStepsSource from '!!raw-loader!./example-zitadel-init-steps.yaml'
-import NoteInstanceNotFound from '../troubleshooting/_note_instance_not_found.mdx';
-With this example configuration, you create a near production environment for ZITADEL with [Docker Compose](https://docs.docker.com/compose/).
-
-The stack consists of three long-running containers:
-- A [Traefik](https://doc.traefik.io/traefik/) reverse proxy with upstream HTTP/2 enabled, issuing a self-signed TLS certificate.
-- A secure ZITADEL container configured for a custom domain. As we terminate TLS with Traefik, we configure ZITADEL for `--tlsMode external`.
+The stack consists of four long-running containers and a couple of short-lived containers:
+- A [Traefik](https://doc.traefik.io/traefik/) reverse proxy container with upstream HTTP/2 enabled, issuing a self-signed TLS certificate.
+- A Login container that is accessible via Traefik at `/ui/v2/login`
+- A Zitadel container that is accessible via Traefik at all other paths than `/ui/v2/login`.
- An insecure [PostgreSQL](https://www.postgresql.org/docs/current/index.html).
-The setup is tested against Docker version 20.10.17 and Docker Compose version v2.2.3
+The Traefik container and the login container call the Zitadel container via the internal Docker network at `h2c://zitadel:8080`
+
+The setup is tested against Docker version 28.0.4 and Docker Compose version v2.34.0
By executing the commands below, you will download the following files:
@@ -64,22 +64,11 @@ tr -dc A-Za-z0-9 ./zitadel-masterkey
export ZITADEL_MASTERKEY="$(cat ./zitadel-masterkey)"
# Run the database and application containers
-docker compose up --detach
+docker compose up --detach --wait
```
-Make `127.0.0.1` available at `my.domain`. For example, this can be achieved with an entry `127.0.0.1 my.domain` in the `/etc/hosts` file.
-
-Open your favorite internet browser at [https://my.domain/ui/console/](https://my.domain/ui/console/).
-You can safely proceed, if your browser warns you about the insecure self-signed TLS certificate.
-This is the IAM admin users login according to your configuration in the [example-zitadel-init-steps.yaml](./example-zitadel-init-steps.yaml):
-- **username**: *root@ my-org.my.domain*
-- **password**: *RootPassword1!*
+Open your favorite internet browser at https://127.0.0.1.sslip.io/ui/console?login_hint=zitadel-admin@zitadel.127.0.0.1.sslip.io.
+Your browser warns you about the insecure self-signed TLS certificate. As 127.0.0.1.sslip.io resolves to your localhost, you can safely proceed.
+Use the password *Password1!* to log in.
Read more about [the login process](/guides/integrate/login/oidc/login-users).
-
-
-
-## Troubleshooting
-
-You can connect to the database like this: `docker exec -it loadbalancing-example-db-1 psql --host localhost`
-For example, to show all login names: `docker exec -it loadbalancing-example-db-1 psql -d zitadel --host localhost -c 'select * from projections.login_names3'`
From fa3efd9da3ad998242ad680e803f6c2bb256cb9f Mon Sep 17 00:00:00 2001
From: Elio Bischof
Date: Tue, 29 Apr 2025 16:33:23 +0200
Subject: [PATCH 026/181] docs: fix Illegal byte sequence (#9750)
# Which Problems Are Solved
In some docs pages, we propose to generate a Zitadel masterkey using the
command `tr -dc A-Za-z0-9 ./zitadel-masterkey
+LC_ALL=C tr -dc '[:graph:]' ./zitadel-masterkey
export ZITADEL_MASTERKEY="$(cat ./zitadel-masterkey)"
# Run the database and application containers
diff --git a/docs/docs/self-hosting/manage/configure/_compose.mdx b/docs/docs/self-hosting/manage/configure/_compose.mdx
index 5e8b1c3937..837d4c6e62 100644
--- a/docs/docs/self-hosting/manage/configure/_compose.mdx
+++ b/docs/docs/self-hosting/manage/configure/_compose.mdx
@@ -43,7 +43,7 @@ wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosti
# A single ZITADEL instance always needs the same 32 bytes long masterkey
# Generate one to a file if you haven't done so already and pass it as environment variable
-tr -dc A-Za-z0-9 ./zitadel-masterkey
+LC_ALL=C tr -dc '[:graph:]' ./zitadel-masterkey
export ZITADEL_MASTERKEY="$(cat ./zitadel-masterkey)"
# Run the database and application containers
diff --git a/docs/docs/self-hosting/manage/configure/_linuxunix.mdx b/docs/docs/self-hosting/manage/configure/_linuxunix.mdx
index 6be833caea..65130ea195 100644
--- a/docs/docs/self-hosting/manage/configure/_linuxunix.mdx
+++ b/docs/docs/self-hosting/manage/configure/_linuxunix.mdx
@@ -35,7 +35,7 @@ wget https://raw.githubusercontent.com/zitadel/zitadel/main/docs/docs/self-hosti
# A single ZITADEL instance always needs the same 32 characters long masterkey
# If you haven't done so already, you can generate a new one
# The key must be passed as argument
-ZITADEL_MASTERKEY="$(tr -dc A-Za-z0-9 ./zitadel-masterkey
+LC_ALL=C tr -dc '[:graph:]' ./zitadel-masterkey
# Let the zitadel binary read configuration from environment variables
zitadel start-from-init --masterkey "${ZITADEL_MASTERKEY}" --tlsMode disabled --masterkeyFile ./zitadel-masterkey
From 91bc71db74fbfd6945822418a5fa8df4439f5757 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tim=20M=C3=B6hlmann?=
Date: Tue, 29 Apr 2025 16:54:53 +0200
Subject: [PATCH 027/181] fix(instance): add web key generation to instance
defaults (#9815)
# Which Problems Are Solved
Webkeys were not generated with new instances when the webkey feature
flag was enabled for instance defaults. This would cause a redirect loop
with console for new instances on QA / coud.
# How the Problems Are Solved
- uncomment the webkeys section on defaults.yaml
- Fix field naming of webkey config
# Additional Changes
- Add all available features as comments.
- Make the improved performance type enum parsable from the config,
untill now they were just ints.
- Running of the enumer command created missing enum entries for feature
keys.
# Additional Context
- Needs to be back-ported to v3 / next-rc
Co-authored-by: Livio Spring
---
cmd/defaults.yaml | 31 +++--
internal/api/grpc/feature/v2/converter.go | 6 +-
internal/api/grpc/feature/v2beta/converter.go | 6 +-
internal/feature/feature.go | 3 +-
.../feature/improvedperformancetype_enumer.go | 106 ++++++++++++++++++
internal/feature/key_enumer.go | 66 +++++------
6 files changed, 171 insertions(+), 47 deletions(-)
create mode 100644 internal/feature/improvedperformancetype_enumer.go
diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml
index 55e14bbada..f20fbc03fc 100644
--- a/cmd/defaults.yaml
+++ b/cmd/defaults.yaml
@@ -959,16 +959,15 @@ DefaultInstance:
EmailTemplate: CjwhZG9jdHlwZSBodG1sPgo8aHRtbCB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94aHRtbCIgeG1sbnM6dj0idXJuOnNjaGVtYXMtbWljcm9zb2Z0LWNvbTp2bWwiIHhtbG5zOm89InVybjpzY2hlbWFzLW1pY3Jvc29mdC1jb206b2ZmaWNlOm9mZmljZSI+CjxoZWFkPgogIDx0aXRsZT4KCiAgPC90aXRsZT4KICA8IS0tW2lmICFtc29dPjwhLS0+CiAgPG1ldGEgaHR0cC1lcXVpdj0iWC1VQS1Db21wYXRpYmxlIiBjb250ZW50PSJJRT1lZGdlIj4KICA8IS0tPCFbZW5kaWZdLS0+CiAgPG1ldGEgaHR0cC1lcXVpdj0iQ29udGVudC1UeXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7IGNoYXJzZXQ9VVRGLTgiPgogIDxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIGluaXRpYWwtc2NhbGU9MSI+CiAgPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KICAgICNvdXRsb29rIGEgeyBwYWRkaW5nOjA7IH0KICAgIGJvZHkgeyBtYXJnaW46MDtwYWRkaW5nOjA7LXdlYmtpdC10ZXh0LXNpemUtYWRqdXN0OjEwMCU7LW1zLXRleHQtc2l6ZS1hZGp1c3Q6MTAwJTsgfQogICAgdGFibGUsIHRkIHsgYm9yZGVyLWNvbGxhcHNlOmNvbGxhcHNlO21zby10YWJsZS1sc3BhY2U6MHB0O21zby10YWJsZS1yc3BhY2U6MHB0OyB9CiAgICBpbWcgeyBib3JkZXI6MDtoZWlnaHQ6YXV0bztsaW5lLWhlaWdodDoxMDAlOyBvdXRsaW5lOm5vbmU7dGV4dC1kZWNvcmF0aW9uOm5vbmU7LW1zLWludGVycG9sYXRpb24tbW9kZTpiaWN1YmljOyB9CiAgICBwIHsgZGlzcGxheTpibG9jazttYXJnaW46MTNweCAwOyB9CiAgPC9zdHlsZT4KICA8IS0tW2lmIG1zb10+CiAgPHhtbD4KICAgIDxvOk9mZmljZURvY3VtZW50U2V0dGluZ3M+CiAgICAgIDxvOkFsbG93UE5HLz4KICAgICAgPG86UGl4ZWxzUGVySW5jaD45NjwvbzpQaXhlbHNQZXJJbmNoPgogICAgPC9vOk9mZmljZURvY3VtZW50U2V0dGluZ3M+CiAgPC94bWw+CiAgPCFbZW5kaWZdLS0+CiAgPCEtLVtpZiBsdGUgbXNvIDExXT4KICA8c3R5bGUgdHlwZT0idGV4dC9jc3MiPgogICAgLm1qLW91dGxvb2stZ3JvdXAtZml4IHsgd2lkdGg6MTAwJSAhaW1wb3J0YW50OyB9CiAgPC9zdHlsZT4KICA8IVtlbmRpZl0tLT4KCgogIDxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+CiAgICBAbWVkaWEgb25seSBzY3JlZW4gYW5kIChtaW4td2lkdGg6NDgwcHgpIHsKICAgICAgLm1qLWNvbHVtbi1wZXItMTAwIHsgd2lkdGg6MTAwJSAhaW1wb3J0YW50OyBtYXgtd2lkdGg6IDEwMCU7IH0KICAgICAgLm1qLWNvbHVtbi1wZXItNjAgeyB3aWR0aDo2MCUgIWltcG9ydGFudDsgbWF4LXdpZHRoOiA2MCU7IH0KICAgIH0KICA8L3N0eWxlPgoKCiAgPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCgoKICAgIEBtZWRpYSBvbmx5IHNjcmVlbiBhbmQgKG1heC13aWR0aDo0ODBweCkgewogICAgICB0YWJsZS5tai1mdWxsLXdpZHRoLW1vYmlsZSB7IHdpZHRoOiAxMDAlICFpbXBvcnRhbnQ7IH0KICAgICAgdGQubWotZnVsbC13aWR0aC1tb2JpbGUgeyB3aWR0aDogYXV0byAhaW1wb3J0YW50OyB9CiAgICB9CgogIDwvc3R5bGU+CiAgPHN0eWxlIHR5cGU9InRleHQvY3NzIj4uc2hhZG93IGEgewogICAgYm94LXNoYWRvdzogMHB4IDNweCAxcHggLTJweCByZ2JhKDAsIDAsIDAsIDAuMiksIDBweCAycHggMnB4IDBweCByZ2JhKDAsIDAsIDAsIDAuMTQpLCAwcHggMXB4IDVweCAwcHggcmdiYSgwLCAwLCAwLCAwLjEyKTsKICB9PC9zdHlsZT4KCiAge3tpZiAuRm9udFVSTH19CiAgPHN0eWxlPgogICAgQGZvbnQtZmFjZSB7CiAgICAgIGZvbnQtZmFtaWx5OiAne3suRm9udEZhY2VGYW1pbHl9fSc7CiAgICAgIGZvbnQtc3R5bGU6IG5vcm1hbDsKICAgICAgZm9udC1kaXNwbGF5OiBzd2FwOwogICAgICBzcmM6IHVybCh7ey5Gb250VVJMfX0pOwogICAgfQogIDwvc3R5bGU+CiAge3tlbmR9fQoKPC9oZWFkPgo8Ym9keSBzdHlsZT0id29yZC1zcGFjaW5nOm5vcm1hbDsiPgoKCjxkaXYKICAgICAgICBzdHlsZT0iIgo+CgogIDx0YWJsZQogICAgICAgICAgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9ImJhY2tncm91bmQ6e3suQmFja2dyb3VuZENvbG9yfX07YmFja2dyb3VuZC1jb2xvcjp7ey5CYWNrZ3JvdW5kQ29sb3J9fTt3aWR0aDoxMDAlO2JvcmRlci1yYWRpdXM6MTZweDsiCiAgPgogICAgPHRib2R5PgogICAgPHRyPgogICAgICA8dGQ+CgoKICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48dGFibGUgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIGNsYXNzPSIiIHN0eWxlPSJ3aWR0aDo4MDBweDsiIHdpZHRoPSI4MDAiID48dHI+PHRkIHN0eWxlPSJsaW5lLWhlaWdodDowcHg7Zm9udC1zaXplOjBweDttc28tbGluZS1oZWlnaHQtcnVsZTpleGFjdGx5OyI+PCFbZW5kaWZdLS0+CgoKICAgICAgICA8ZGl2ICBzdHlsZT0ibWFyZ2luOjBweCBhdXRvO2JvcmRlci1yYWRpdXM6MTZweDttYXgtd2lkdGg6ODAwcHg7Ij4KCiAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9IndpZHRoOjEwMCU7Ym9yZGVyLXJhZGl1czoxNnB4OyIKICAgICAgICAgID4KICAgICAgICAgICAgPHRib2R5PgogICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICBzdHlsZT0iZGlyZWN0aW9uOmx0cjtmb250LXNpemU6MHB4O3BhZGRpbmc6MjBweCAwO3BhZGRpbmctbGVmdDowO3RleHQtYWxpZ246Y2VudGVyOyIKICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48dGFibGUgcm9sZT0icHJlc2VudGF0aW9uIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCI+PHRyPjx0ZCBjbGFzcz0iIiB3aWR0aD0iODAwcHgiID48IVtlbmRpZl0tLT4KCiAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9IndpZHRoOjEwMCU7IgogICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICA8dGQ+CgoKICAgICAgICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjx0YWJsZSBhbGlnbj0iY2VudGVyIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgY2xhc3M9IiIgc3R5bGU9IndpZHRoOjgwMHB4OyIgd2lkdGg9IjgwMCIgPjx0cj48dGQgc3R5bGU9ImxpbmUtaGVpZ2h0OjBweDtmb250LXNpemU6MHB4O21zby1saW5lLWhlaWdodC1ydWxlOmV4YWN0bHk7Ij48IVtlbmRpZl0tLT4KCgogICAgICAgICAgICAgICAgICAgICAgPGRpdiAgc3R5bGU9Im1hcmdpbjowcHggYXV0bzttYXgtd2lkdGg6ODAwcHg7Ij4KCiAgICAgICAgICAgICAgICAgICAgICAgIDx0YWJsZQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFsaWduPSJjZW50ZXIiIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHN0eWxlPSJ3aWR0aDoxMDAlOyIKICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3R5bGU9ImRpcmVjdGlvbjpsdHI7Zm9udC1zaXplOjBweDtwYWRkaW5nOjA7dGV4dC1hbGlnbjpjZW50ZXI7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48dGFibGUgcm9sZT0icHJlc2VudGF0aW9uIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCI+PHRyPjx0ZCBjbGFzcz0iIiBzdHlsZT0id2lkdGg6ODAwcHg7IiA+PCFbZW5kaWZdLS0+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGl2CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3M9Im1qLWNvbHVtbi1wZXItMTAwIG1qLW91dGxvb2stZ3JvdXAtZml4IiBzdHlsZT0iZm9udC1zaXplOjA7bGluZS1oZWlnaHQ6MDt0ZXh0LWFsaWduOmxlZnQ7ZGlzcGxheTppbmxpbmUtYmxvY2s7d2lkdGg6MTAwJTtkaXJlY3Rpb246bHRyOyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjx0YWJsZSBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiA+PHRyPjx0ZCBzdHlsZT0idmVydGljYWwtYWxpZ246dG9wO3dpZHRoOjgwMHB4OyIgPjwhW2VuZGlmXS0tPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8ZGl2CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBjbGFzcz0ibWotY29sdW1uLXBlci0xMDAgbWotb3V0bG9vay1ncm91cC1maXgiIHN0eWxlPSJmb250LXNpemU6MHB4O3RleHQtYWxpZ246bGVmdDtkaXJlY3Rpb246bHRyO2Rpc3BsYXk6aW5saW5lLWJsb2NrO3ZlcnRpY2FsLWFsaWduOnRvcDt3aWR0aDoxMDAlOyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHdpZHRoPSIxMDAlIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQgIHN0eWxlPSJ2ZXJ0aWNhbC1hbGlnbjp0b3A7cGFkZGluZzowOyI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB7e2lmIC5Mb2dvVVJMfX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0YWJsZQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0iIiB3aWR0aD0iMTAwJSIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRib2R5PgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7cGFkZGluZzo1MHB4IDAgMzBweCAwO3dvcmQtYnJlYWs6YnJlYWstd29yZDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0YWJsZQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0iYm9yZGVyLWNvbGxhcHNlOmNvbGxhcHNlO2JvcmRlci1zcGFjaW5nOjBweDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCAgc3R5bGU9IndpZHRoOjE4MHB4OyI+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGltZwogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBoZWlnaHQ9ImF1dG8iIHNyYz0ie3suTG9nb1VSTH19IiBzdHlsZT0iYm9yZGVyOjA7Ym9yZGVyLXJhZGl1czo4cHg7ZGlzcGxheTpibG9jaztvdXRsaW5lOm5vbmU7dGV4dC1kZWNvcmF0aW9uOm5vbmU7aGVpZ2h0OmF1dG87d2lkdGg6MTAwJTtmb250LXNpemU6MTNweDsiIHdpZHRoPSIxODAiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAvPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAge3tlbmR9fQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+PC90ZD48L3RyPjwvdGFibGU+PCFbZW5kaWZdLS0+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvZGl2PgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+PC90ZD48L3RyPjwvdGFibGU+PCFbZW5kaWZdLS0+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgPC90Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KCiAgICAgICAgICAgICAgICAgICAgICA8L2Rpdj4KCgogICAgICAgICAgICAgICAgICAgICAgPCEtLVtpZiBtc28gfCBJRV0+PC90ZD48L3RyPjwvdGFibGU+PCFbZW5kaWZdLS0+CgoKICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgPC90YWJsZT4KCiAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48L3RkPjwvdHI+PHRyPjx0ZCBjbGFzcz0iIiB3aWR0aD0iODAwcHgiID48IVtlbmRpZl0tLT4KCiAgICAgICAgICAgICAgICA8dGFibGUKICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9IndpZHRoOjEwMCU7IgogICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICA8dGQ+CgoKICAgICAgICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjx0YWJsZSBhbGlnbj0iY2VudGVyIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgY2xhc3M9IiIgc3R5bGU9IndpZHRoOjgwMHB4OyIgd2lkdGg9IjgwMCIgPjx0cj48dGQgc3R5bGU9ImxpbmUtaGVpZ2h0OjBweDtmb250LXNpemU6MHB4O21zby1saW5lLWhlaWdodC1ydWxlOmV4YWN0bHk7Ij48IVtlbmRpZl0tLT4KCgogICAgICAgICAgICAgICAgICAgICAgPGRpdiAgc3R5bGU9Im1hcmdpbjowcHggYXV0bzttYXgtd2lkdGg6ODAwcHg7Ij4KCiAgICAgICAgICAgICAgICAgICAgICAgIDx0YWJsZQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFsaWduPSJjZW50ZXIiIGJvcmRlcj0iMCIgY2VsbHBhZGRpbmc9IjAiIGNlbGxzcGFjaW5nPSIwIiByb2xlPSJwcmVzZW50YXRpb24iIHN0eWxlPSJ3aWR0aDoxMDAlOyIKICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgIDx0Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3R5bGU9ImRpcmVjdGlvbjpsdHI7Zm9udC1zaXplOjBweDtwYWRkaW5nOjA7dGV4dC1hbGlnbjpjZW50ZXI7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48dGFibGUgcm9sZT0icHJlc2VudGF0aW9uIiBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCI+PHRyPjx0ZCBjbGFzcz0iIiBzdHlsZT0idmVydGljYWwtYWxpZ246dG9wO3dpZHRoOjQ4MHB4OyIgPjwhW2VuZGlmXS0tPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGRpdgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGNsYXNzPSJtai1jb2x1bW4tcGVyLTYwIG1qLW91dGxvb2stZ3JvdXAtZml4IiBzdHlsZT0iZm9udC1zaXplOjBweDt0ZXh0LWFsaWduOmxlZnQ7ZGlyZWN0aW9uOmx0cjtkaXNwbGF5OmlubGluZS1ibG9jazt2ZXJ0aWNhbC1hbGlnbjp0b3A7d2lkdGg6MTAwJTsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiB3aWR0aD0iMTAwJSIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZCAgc3R5bGU9InZlcnRpY2FsLWFsaWduOnRvcDtwYWRkaW5nOjA7Ij4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0iIiB3aWR0aD0iMTAwJSIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGJvZHk+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dGQKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBhbGlnbj0iY2VudGVyIiBzdHlsZT0iZm9udC1zaXplOjBweDtwYWRkaW5nOjEwcHggMjVweDt3b3JkLWJyZWFrOmJyZWFrLXdvcmQ7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxkaXYKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN0eWxlPSJmb250LWZhbWlseTp7ey5Gb250RmFtaWx5fX07Zm9udC1zaXplOjI0cHg7Zm9udC13ZWlnaHQ6NTAwO2xpbmUtaGVpZ2h0OjE7dGV4dC1hbGlnbjpjZW50ZXI7Y29sb3I6e3suRm9udENvbG9yfX07IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID57ey5HcmVldGluZ319PC9kaXY+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFsaWduPSJjZW50ZXIiIHN0eWxlPSJmb250LXNpemU6MHB4O3BhZGRpbmc6MTBweCAyNXB4O3dvcmQtYnJlYWs6YnJlYWstd29yZDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGRpdgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3R5bGU9ImZvbnQtZmFtaWx5Ont7LkZvbnRGYW1pbHl9fTtmb250LXNpemU6MTZweDtmb250LXdlaWdodDpsaWdodDtsaW5lLWhlaWdodDoxLjU7dGV4dC1hbGlnbjpjZW50ZXI7Y29sb3I6e3suRm9udENvbG9yfX07IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID57ey5UZXh0fX08L2Rpdj4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgoKCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8dHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0ZAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGFsaWduPSJjZW50ZXIiIHZlcnRpY2FsLWFsaWduPSJtaWRkbGUiIGNsYXNzPSJzaGFkb3ciIHN0eWxlPSJmb250LXNpemU6MHB4O3BhZGRpbmc6MTBweCAyNXB4O3dvcmQtYnJlYWs6YnJlYWstd29yZDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRhYmxlCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBib3JkZXI9IjAiIGNlbGxwYWRkaW5nPSIwIiBjZWxsc3BhY2luZz0iMCIgcm9sZT0icHJlc2VudGF0aW9uIiBzdHlsZT0iYm9yZGVyLWNvbGxhcHNlOnNlcGFyYXRlO2xpbmUtaGVpZ2h0OjEwMCU7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgYmdjb2xvcj0ie3suUHJpbWFyeUNvbG9yfX0iIHJvbGU9InByZXNlbnRhdGlvbiIgc3R5bGU9ImJvcmRlcjpub25lO2JvcmRlci1yYWRpdXM6NnB4O2N1cnNvcjphdXRvO21zby1wYWRkaW5nLWFsdDoxMHB4IDI1cHg7YmFja2dyb3VuZDp7ey5QcmltYXJ5Q29sb3J9fTsiIHZhbGlnbj0ibWlkZGxlIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGEKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGhyZWY9Int7LlVSTH19IiByZWw9Im5vb3BlbmVyIG5vcmVmZXJyZXIgbm90cmFjayIgc3R5bGU9ImRpc3BsYXk6aW5saW5lLWJsb2NrO2JhY2tncm91bmQ6e3suUHJpbWFyeUNvbG9yfX07Y29sb3I6I2ZmZmZmZjtmb250LWZhbWlseTp7ey5Gb250RmFtaWx5fX07Zm9udC1zaXplOjE0cHg7Zm9udC13ZWlnaHQ6NTAwO2xpbmUtaGVpZ2h0OjEyMCU7bWFyZ2luOjA7dGV4dC1kZWNvcmF0aW9uOm5vbmU7dGV4dC10cmFuc2Zvcm06bm9uZTtwYWRkaW5nOjEwcHggMjVweDttc28tcGFkZGluZy1hbHQ6MHB4O2JvcmRlci1yYWRpdXM6NnB4OyIgdGFyZ2V0PSJfYmxhbmsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAge3suQnV0dG9uVGV4dH19CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9hPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90ZD4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB7e2lmIC5JbmNsdWRlRm9vdGVyfX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7cGFkZGluZzoxMHB4IDI1cHg7cGFkZGluZy10b3A6MjBweDtwYWRkaW5nLXJpZ2h0OjIwcHg7cGFkZGluZy1ib3R0b206MjBweDtwYWRkaW5nLWxlZnQ6MjBweDt3b3JkLWJyZWFrOmJyZWFrLXdvcmQ7IgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDxwCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdHlsZT0iYm9yZGVyLXRvcDpzb2xpZCAycHggI2RiZGJkYjtmb250LXNpemU6MXB4O21hcmdpbjowcHggYXV0bzt3aWR0aDoxMDAlOyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9wPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48dGFibGUgYWxpZ249ImNlbnRlciIgYm9yZGVyPSIwIiBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiIHN0eWxlPSJib3JkZXItdG9wOnNvbGlkIDJweCAjZGJkYmRiO2ZvbnQtc2l6ZToxcHg7bWFyZ2luOjBweCBhdXRvO3dpZHRoOjQ0MHB4OyIgcm9sZT0icHJlc2VudGF0aW9uIiB3aWR0aD0iNDQwcHgiID48dHI+PHRkIHN0eWxlPSJoZWlnaHQ6MDtsaW5lLWhlaWdodDowOyI+ICZuYnNwOwogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+PC90cj48L3RhYmxlPjwhW2VuZGlmXS0tPgoKCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDx0cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPHRkCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYWxpZ249ImNlbnRlciIgc3R5bGU9ImZvbnQtc2l6ZTowcHg7cGFkZGluZzoxNnB4O3dvcmQtYnJlYWs6YnJlYWstd29yZDsiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgID4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPGRpdgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc3R5bGU9ImZvbnQtZmFtaWx5Ont7LkZvbnRGYW1pbHl9fTtmb250LXNpemU6MTNweDtsaW5lLWhlaWdodDoxO3RleHQtYWxpZ246Y2VudGVyO2NvbG9yOnt7LkZvbnRDb2xvcn19OyIKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA+e3suRm9vdGVyVGV4dH19PC9kaXY+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RkPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHt7ZW5kfX0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90YWJsZT4KCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RyPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC90Ym9keT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgPC9kaXY+CgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48L3RkPjwvdHI+PC90YWJsZT48IVtlbmRpZl0tLT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgICAgICAgICAgPC90cj4KICAgICAgICAgICAgICAgICAgICAgICAgICA8L3Rib2R5PgogICAgICAgICAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgICAgICAgIDwvZGl2PgoKCiAgICAgICAgICAgICAgICAgICAgICA8IS0tW2lmIG1zbyB8IElFXT48L3RkPjwvdHI+PC90YWJsZT48IVtlbmRpZl0tLT4KCgogICAgICAgICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICAgICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjwvdGQ+PC90cj48L3RhYmxlPjwhW2VuZGlmXS0tPgogICAgICAgICAgICAgIDwvdGQ+CiAgICAgICAgICAgIDwvdHI+CiAgICAgICAgICAgIDwvdGJvZHk+CiAgICAgICAgICA8L3RhYmxlPgoKICAgICAgICA8L2Rpdj4KCgogICAgICAgIDwhLS1baWYgbXNvIHwgSUVdPjwvdGQ+PC90cj48L3RhYmxlPjwhW2VuZGlmXS0tPgoKCiAgICAgIDwvdGQ+CiAgICA8L3RyPgogICAgPC90Ym9keT4KICA8L3RhYmxlPgoKPC9kaXY+Cgo8L2JvZHk+CjwvaHRtbD4K # ZITADEL_DEFAULTINSTANCE_EMAILTEMPLATE
# WebKeys configures the OIDC token signing keys that are generated when a new instance is created.
- # WebKeys are still in alpha, so the config is disabled here. This will prevent generation of keys for now.
- # WebKeys:
- # Type: "rsa" # ZITADEL_DEFAULTINSTANCE_WEBKEYS_TYPE
- # Config:
- # Bits: "2048" # ZITADEL_DEFAULTINSTANCE_WEBKEYS_CONFIG_BITS
- # Hasher: "sha256" # ZITADEL_DEFAULTINSTANCE_WEBKEYS_CONFIG_HASHER
+ WebKeys:
+ Type: "rsa" # ZITADEL_DEFAULTINSTANCE_WEBKEYS_TYPE
+ Config:
+ RSABits: "2048" # ZITADEL_DEFAULTINSTANCE_WEBKEYS_CONFIG_BITS
+ RSAHasher: "sha256" # ZITADEL_DEFAULTINSTANCE_WEBKEYS_CONFIG_HASHER
# WebKeys:
# Type: "ecdsa"
# Config:
- # Curve: "P256" # ZITADEL_DEFAULTINSTANCE_WEBKEYS_CONFIG_CURVE
+ # EllipticCurve: "P256" # ZITADEL_DEFAULTINSTANCE_WEBKEYS_CONFIG_CURVE
# Sets the default values for lifetime and expiration for OIDC in each newly created instance
# This default can be overwritten for each instance during runtime
@@ -1101,7 +1100,25 @@ DefaultInstance:
LoginDefaultOrg: true # ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINDEFAULTORG
# TriggerIntrospectionProjections: false # ZITADEL_DEFAULTINSTANCE_FEATURES_TRIGGERINTROSPECTIONPROJECTIONS
# LegacyIntrospection: false # ZITADEL_DEFAULTINSTANCE_FEATURES_LEGACYINTROSPECTION
+ # UserSchema: false # ZITADEL_DEFAULTINSTANCE_FEATURES_USERSCHEMA
+ # TokenExchange: false # ZITADEL_DEFAULTINSTANCE_FEATURES_TOKENEXCHANGE
+ # ImprovedPerformance: # ZITADEL_DEFAULTINSTANCE_FEATURES_IMPROVEDPERFORMANCE
+ # - OrgByID
+ # - ProjectGrant
+ # - Project
+ # - UserGrant
+ # - OrgDomainVerified
+ # WebKey: false # ZITADEL_DEFAULTINSTANCE_FEATURES_WEBKEY
+ # DebugOIDCParentError: false # ZITADEL_DEFAULTINSTANCE_FEATURES_DEBUGOIDCPARENTERROR
+ # OIDCSingleV1SessionTermination: false # ZITADEL_DEFAULTINSTANCE_FEATURES_OIDCSINGLEV1SESSIONTERMINATION
+ # DisableUserTokenEvent: false # ZITADEL_DEFAULTINSTANCE_FEATURES_DISABLEUSERTOKENEVENT
+ # EnableBackChannelLogout: false # ZITADEL_DEFAULTINSTANCE_FEATURES_ENABLEBACKCHANNELLOGOUT
+ # LoginV2:
+ # Required: false # ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED
+ # BaseURI: "" # ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI
# PermissionCheckV2: false # ZITADEL_DEFAULTINSTANCE_FEATURES_PERMISSIONCHECKV2
+ # ConsoleUseV2UserApi: false # ZITADEL_DEFAULTINSTANCE_FEATURES_CONSOLEUSEV2USERAPI
+
Limits:
# AuditLogRetention limits the number of events that can be queried via the events API by their age.
# A value of "0s" means that all events are available.
diff --git a/internal/api/grpc/feature/v2/converter.go b/internal/api/grpc/feature/v2/converter.go
index baa45c6c6e..e146ac2db6 100644
--- a/internal/api/grpc/feature/v2/converter.go
+++ b/internal/api/grpc/feature/v2/converter.go
@@ -172,7 +172,7 @@ func improvedPerformanceTypesToPb(types []feature.ImprovedPerformanceType) []fea
func improvedPerformanceTypeToPb(typ feature.ImprovedPerformanceType) feature_pb.ImprovedPerformance {
switch typ {
- case feature.ImprovedPerformanceTypeUnknown:
+ case feature.ImprovedPerformanceTypeUnspecified:
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_UNSPECIFIED
case feature.ImprovedPerformanceTypeOrgByID:
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID
@@ -205,7 +205,7 @@ func improvedPerformanceListToDomain(list []feature_pb.ImprovedPerformance) []fe
func improvedPerformanceToDomain(typ feature_pb.ImprovedPerformance) feature.ImprovedPerformanceType {
switch typ {
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_UNSPECIFIED:
- return feature.ImprovedPerformanceTypeUnknown
+ return feature.ImprovedPerformanceTypeUnspecified
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID:
return feature.ImprovedPerformanceTypeOrgByID
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT_GRANT:
@@ -217,6 +217,6 @@ func improvedPerformanceToDomain(typ feature_pb.ImprovedPerformance) feature.Imp
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_DOMAIN_VERIFIED:
return feature.ImprovedPerformanceTypeOrgDomainVerified
default:
- return feature.ImprovedPerformanceTypeUnknown
+ return feature.ImprovedPerformanceTypeUnspecified
}
}
diff --git a/internal/api/grpc/feature/v2beta/converter.go b/internal/api/grpc/feature/v2beta/converter.go
index bbb375716e..9739e1c4c8 100644
--- a/internal/api/grpc/feature/v2beta/converter.go
+++ b/internal/api/grpc/feature/v2beta/converter.go
@@ -109,7 +109,7 @@ func improvedPerformanceTypesToPb(types []feature.ImprovedPerformanceType) []fea
func improvedPerformanceTypeToPb(typ feature.ImprovedPerformanceType) feature_pb.ImprovedPerformance {
switch typ {
- case feature.ImprovedPerformanceTypeUnknown:
+ case feature.ImprovedPerformanceTypeUnspecified:
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_UNSPECIFIED
case feature.ImprovedPerformanceTypeOrgByID:
return feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID
@@ -142,7 +142,7 @@ func improvedPerformanceListToDomain(list []feature_pb.ImprovedPerformance) []fe
func improvedPerformanceToDomain(typ feature_pb.ImprovedPerformance) feature.ImprovedPerformanceType {
switch typ {
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_UNSPECIFIED:
- return feature.ImprovedPerformanceTypeUnknown
+ return feature.ImprovedPerformanceTypeUnspecified
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID:
return feature.ImprovedPerformanceTypeOrgByID
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_PROJECT_GRANT:
@@ -154,6 +154,6 @@ func improvedPerformanceToDomain(typ feature_pb.ImprovedPerformance) feature.Imp
case feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_DOMAIN_VERIFIED:
return feature.ImprovedPerformanceTypeOrgDomainVerified
default:
- return feature.ImprovedPerformanceTypeUnknown
+ return feature.ImprovedPerformanceTypeUnspecified
}
}
diff --git a/internal/feature/feature.go b/internal/feature/feature.go
index 389b750483..f500b80eb3 100644
--- a/internal/feature/feature.go
+++ b/internal/feature/feature.go
@@ -57,10 +57,11 @@ type Features struct {
ConsoleUseV2UserApi bool `json:"console_use_v2_user_api,omitempty"`
}
+//go:generate enumer -type ImprovedPerformanceType -trimprefix ImprovedPerformanceType -text
type ImprovedPerformanceType int32
const (
- ImprovedPerformanceTypeUnknown = iota
+ ImprovedPerformanceTypeUnspecified ImprovedPerformanceType = iota
ImprovedPerformanceTypeOrgByID
ImprovedPerformanceTypeProjectGrant
ImprovedPerformanceTypeProject
diff --git a/internal/feature/improvedperformancetype_enumer.go b/internal/feature/improvedperformancetype_enumer.go
new file mode 100644
index 0000000000..a12673c205
--- /dev/null
+++ b/internal/feature/improvedperformancetype_enumer.go
@@ -0,0 +1,106 @@
+// Code generated by "enumer -type ImprovedPerformanceType -trimprefix ImprovedPerformanceType -text"; DO NOT EDIT.
+
+package feature
+
+import (
+ "fmt"
+ "strings"
+)
+
+const _ImprovedPerformanceTypeName = "UnspecifiedOrgByIDProjectGrantProjectUserGrantOrgDomainVerified"
+
+var _ImprovedPerformanceTypeIndex = [...]uint8{0, 11, 18, 30, 37, 46, 63}
+
+const _ImprovedPerformanceTypeLowerName = "unspecifiedorgbyidprojectgrantprojectusergrantorgdomainverified"
+
+func (i ImprovedPerformanceType) String() string {
+ if i < 0 || i >= ImprovedPerformanceType(len(_ImprovedPerformanceTypeIndex)-1) {
+ return fmt.Sprintf("ImprovedPerformanceType(%d)", i)
+ }
+ return _ImprovedPerformanceTypeName[_ImprovedPerformanceTypeIndex[i]:_ImprovedPerformanceTypeIndex[i+1]]
+}
+
+// An "invalid array index" compiler error signifies that the constant values have changed.
+// Re-run the stringer command to generate them again.
+func _ImprovedPerformanceTypeNoOp() {
+ var x [1]struct{}
+ _ = x[ImprovedPerformanceTypeUnspecified-(0)]
+ _ = x[ImprovedPerformanceTypeOrgByID-(1)]
+ _ = x[ImprovedPerformanceTypeProjectGrant-(2)]
+ _ = x[ImprovedPerformanceTypeProject-(3)]
+ _ = x[ImprovedPerformanceTypeUserGrant-(4)]
+ _ = x[ImprovedPerformanceTypeOrgDomainVerified-(5)]
+}
+
+var _ImprovedPerformanceTypeValues = []ImprovedPerformanceType{ImprovedPerformanceTypeUnspecified, ImprovedPerformanceTypeOrgByID, ImprovedPerformanceTypeProjectGrant, ImprovedPerformanceTypeProject, ImprovedPerformanceTypeUserGrant, ImprovedPerformanceTypeOrgDomainVerified}
+
+var _ImprovedPerformanceTypeNameToValueMap = map[string]ImprovedPerformanceType{
+ _ImprovedPerformanceTypeName[0:11]: ImprovedPerformanceTypeUnspecified,
+ _ImprovedPerformanceTypeLowerName[0:11]: ImprovedPerformanceTypeUnspecified,
+ _ImprovedPerformanceTypeName[11:18]: ImprovedPerformanceTypeOrgByID,
+ _ImprovedPerformanceTypeLowerName[11:18]: ImprovedPerformanceTypeOrgByID,
+ _ImprovedPerformanceTypeName[18:30]: ImprovedPerformanceTypeProjectGrant,
+ _ImprovedPerformanceTypeLowerName[18:30]: ImprovedPerformanceTypeProjectGrant,
+ _ImprovedPerformanceTypeName[30:37]: ImprovedPerformanceTypeProject,
+ _ImprovedPerformanceTypeLowerName[30:37]: ImprovedPerformanceTypeProject,
+ _ImprovedPerformanceTypeName[37:46]: ImprovedPerformanceTypeUserGrant,
+ _ImprovedPerformanceTypeLowerName[37:46]: ImprovedPerformanceTypeUserGrant,
+ _ImprovedPerformanceTypeName[46:63]: ImprovedPerformanceTypeOrgDomainVerified,
+ _ImprovedPerformanceTypeLowerName[46:63]: ImprovedPerformanceTypeOrgDomainVerified,
+}
+
+var _ImprovedPerformanceTypeNames = []string{
+ _ImprovedPerformanceTypeName[0:11],
+ _ImprovedPerformanceTypeName[11:18],
+ _ImprovedPerformanceTypeName[18:30],
+ _ImprovedPerformanceTypeName[30:37],
+ _ImprovedPerformanceTypeName[37:46],
+ _ImprovedPerformanceTypeName[46:63],
+}
+
+// ImprovedPerformanceTypeString retrieves an enum value from the enum constants string name.
+// Throws an error if the param is not part of the enum.
+func ImprovedPerformanceTypeString(s string) (ImprovedPerformanceType, error) {
+ if val, ok := _ImprovedPerformanceTypeNameToValueMap[s]; ok {
+ return val, nil
+ }
+
+ if val, ok := _ImprovedPerformanceTypeNameToValueMap[strings.ToLower(s)]; ok {
+ return val, nil
+ }
+ return 0, fmt.Errorf("%s does not belong to ImprovedPerformanceType values", s)
+}
+
+// ImprovedPerformanceTypeValues returns all values of the enum
+func ImprovedPerformanceTypeValues() []ImprovedPerformanceType {
+ return _ImprovedPerformanceTypeValues
+}
+
+// ImprovedPerformanceTypeStrings returns a slice of all String values of the enum
+func ImprovedPerformanceTypeStrings() []string {
+ strs := make([]string, len(_ImprovedPerformanceTypeNames))
+ copy(strs, _ImprovedPerformanceTypeNames)
+ return strs
+}
+
+// IsAImprovedPerformanceType returns "true" if the value is listed in the enum definition. "false" otherwise
+func (i ImprovedPerformanceType) IsAImprovedPerformanceType() bool {
+ for _, v := range _ImprovedPerformanceTypeValues {
+ if i == v {
+ return true
+ }
+ }
+ return false
+}
+
+// MarshalText implements the encoding.TextMarshaler interface for ImprovedPerformanceType
+func (i ImprovedPerformanceType) MarshalText() ([]byte, error) {
+ return []byte(i.String()), nil
+}
+
+// UnmarshalText implements the encoding.TextUnmarshaler interface for ImprovedPerformanceType
+func (i *ImprovedPerformanceType) UnmarshalText(text []byte) error {
+ var err error
+ *i, err = ImprovedPerformanceTypeString(string(text))
+ return err
+}
diff --git a/internal/feature/key_enumer.go b/internal/feature/key_enumer.go
index 6466061718..a47b3eb4d9 100644
--- a/internal/feature/key_enumer.go
+++ b/internal/feature/key_enumer.go
@@ -7,11 +7,11 @@ import (
"strings"
)
-const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2console_use_v2_user_api"
+const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactions_deprecatedimproved_performanceweb_keydebug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2console_use_v2_user_api"
-var _KeyIndex = [...]uint16{0, 11, 28, 61, 81, 92, 106, 113, 133, 140, 163, 197, 221, 247, 255, 274, 297}
+var _KeyIndex = [...]uint16{0, 11, 28, 61, 81, 92, 106, 124, 144, 151, 174, 208, 232, 258, 266, 285, 308}
-const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_keydebug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2console_use_v2_user_api"
+const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactions_deprecatedimproved_performanceweb_keydebug_oidc_parent_erroroidc_single_v1_session_terminationdisable_user_token_eventenable_back_channel_logoutlogin_v2permission_check_v2console_use_v2_user_api"
func (i Key) String() string {
if i < 0 || i >= Key(len(_KeyIndex)-1) {
@@ -57,26 +57,26 @@ var _KeyNameToValueMap = map[string]Key{
_KeyLowerName[81:92]: KeyUserSchema,
_KeyName[92:106]: KeyTokenExchange,
_KeyLowerName[92:106]: KeyTokenExchange,
- _KeyName[106:113]: KeyActionsDeprecated,
- _KeyLowerName[106:113]: KeyActionsDeprecated,
- _KeyName[113:133]: KeyImprovedPerformance,
- _KeyLowerName[113:133]: KeyImprovedPerformance,
- _KeyName[133:140]: KeyWebKey,
- _KeyLowerName[133:140]: KeyWebKey,
- _KeyName[140:163]: KeyDebugOIDCParentError,
- _KeyLowerName[140:163]: KeyDebugOIDCParentError,
- _KeyName[163:197]: KeyOIDCSingleV1SessionTermination,
- _KeyLowerName[163:197]: KeyOIDCSingleV1SessionTermination,
- _KeyName[197:221]: KeyDisableUserTokenEvent,
- _KeyLowerName[197:221]: KeyDisableUserTokenEvent,
- _KeyName[221:247]: KeyEnableBackChannelLogout,
- _KeyLowerName[221:247]: KeyEnableBackChannelLogout,
- _KeyName[247:255]: KeyLoginV2,
- _KeyLowerName[247:255]: KeyLoginV2,
- _KeyName[255:274]: KeyPermissionCheckV2,
- _KeyLowerName[255:274]: KeyPermissionCheckV2,
- _KeyName[274:297]: KeyConsoleUseV2UserApi,
- _KeyLowerName[274:297]: KeyConsoleUseV2UserApi,
+ _KeyName[106:124]: KeyActionsDeprecated,
+ _KeyLowerName[106:124]: KeyActionsDeprecated,
+ _KeyName[124:144]: KeyImprovedPerformance,
+ _KeyLowerName[124:144]: KeyImprovedPerformance,
+ _KeyName[144:151]: KeyWebKey,
+ _KeyLowerName[144:151]: KeyWebKey,
+ _KeyName[151:174]: KeyDebugOIDCParentError,
+ _KeyLowerName[151:174]: KeyDebugOIDCParentError,
+ _KeyName[174:208]: KeyOIDCSingleV1SessionTermination,
+ _KeyLowerName[174:208]: KeyOIDCSingleV1SessionTermination,
+ _KeyName[208:232]: KeyDisableUserTokenEvent,
+ _KeyLowerName[208:232]: KeyDisableUserTokenEvent,
+ _KeyName[232:258]: KeyEnableBackChannelLogout,
+ _KeyLowerName[232:258]: KeyEnableBackChannelLogout,
+ _KeyName[258:266]: KeyLoginV2,
+ _KeyLowerName[258:266]: KeyLoginV2,
+ _KeyName[266:285]: KeyPermissionCheckV2,
+ _KeyLowerName[266:285]: KeyPermissionCheckV2,
+ _KeyName[285:308]: KeyConsoleUseV2UserApi,
+ _KeyLowerName[285:308]: KeyConsoleUseV2UserApi,
}
var _KeyNames = []string{
@@ -86,16 +86,16 @@ var _KeyNames = []string{
_KeyName[61:81],
_KeyName[81:92],
_KeyName[92:106],
- _KeyName[106:113],
- _KeyName[113:133],
- _KeyName[133:140],
- _KeyName[140:163],
- _KeyName[163:197],
- _KeyName[197:221],
- _KeyName[221:247],
- _KeyName[247:255],
- _KeyName[255:274],
- _KeyName[274:297],
+ _KeyName[106:124],
+ _KeyName[124:144],
+ _KeyName[144:151],
+ _KeyName[151:174],
+ _KeyName[174:208],
+ _KeyName[208:232],
+ _KeyName[232:258],
+ _KeyName[258:266],
+ _KeyName[266:285],
+ _KeyName[285:308],
}
// KeyString retrieves an enum value from the enum constants string name.
From 181186e477f7ae1779cf2b4429f25e00b9712e98 Mon Sep 17 00:00:00 2001
From: Silvan <27845747+adlerhurst@users.noreply.github.com>
Date: Tue, 29 Apr 2025 17:29:16 +0200
Subject: [PATCH 028/181] fix(mirror): add max auth request age configuration
(#9812)
# Which Problems Are Solved
The `auth.auth_requests` table is not cleaned up so long running Zitadel
installations can contain many rows.
The mirror command can take long because a the data are first copied
into memory (or disk) on cockroach and users do not get any output from
mirror. This is unfortunate because people don't know if Zitadel got
stuck.
# How the Problems Are Solved
Enhance logging throughout the projection processes and introduce a
configuration option for the maximum age of authentication requests.
# Additional Changes
None
# Additional Context
closes https://github.com/zitadel/zitadel/issues/9764
---------
Co-authored-by: Livio Spring
---
cmd/mirror/auth.go | 15 ++-
cmd/mirror/config.go | 3 +-
cmd/mirror/defaults.yaml | 97 ++++++++++---------
cmd/mirror/event_store.go | 5 +
cmd/mirror/projections.go | 10 +-
cmd/mirror/system.go | 14 +--
docs/docs/self-hosting/manage/cli/mirror.mdx | 3 +
.../eventsourcing/handler/handler.go | 8 +-
.../eventsourcing/handler/handler.go | 8 +-
internal/notification/projections.go | 8 +-
internal/query/projection/projection.go | 12 ++-
11 files changed, 116 insertions(+), 67 deletions(-)
diff --git a/cmd/mirror/auth.go b/cmd/mirror/auth.go
index 0eba10d05f..3d7ae45bce 100644
--- a/cmd/mirror/auth.go
+++ b/cmd/mirror/auth.go
@@ -4,6 +4,7 @@ import (
"context"
_ "embed"
"io"
+ "strconv"
"time"
"github.com/jackc/pgx/v5/stdlib"
@@ -41,12 +42,16 @@ func copyAuth(ctx context.Context, config *Migration) {
logging.OnError(err).Fatal("unable to connect to destination database")
defer destClient.Close()
- copyAuthRequests(ctx, sourceClient, destClient)
+ copyAuthRequests(ctx, sourceClient, destClient, config.MaxAuthRequestAge)
}
-func copyAuthRequests(ctx context.Context, source, dest *database.DB) {
+func copyAuthRequests(ctx context.Context, source, dest *database.DB, maxAuthRequestAge time.Duration) {
start := time.Now()
+ logging.Info("creating index on auth.auth_requests.change_date to speed up copy in source database")
+ _, err := source.ExecContext(ctx, "CREATE INDEX CONCURRENTLY IF NOT EXISTS auth_requests_change_date ON auth.auth_requests (change_date)")
+ logging.OnError(err).Fatal("unable to create index on auth.auth_requests.change_date")
+
sourceConn, err := source.Conn(ctx)
logging.OnError(err).Fatal("unable to acquire connection")
defer sourceConn.Close()
@@ -55,9 +60,9 @@ func copyAuthRequests(ctx context.Context, source, dest *database.DB) {
errs := make(chan error, 1)
go func() {
- err = sourceConn.Raw(func(driverConn interface{}) error {
+ err = sourceConn.Raw(func(driverConn any) error {
conn := driverConn.(*stdlib.Conn).Conn()
- _, err := conn.PgConn().CopyTo(ctx, w, "COPY (SELECT id, regexp_replace(request::TEXT, '\\\\u0000', '', 'g')::JSON request, code, request_type, creation_date, change_date, instance_id FROM auth.auth_requests "+instanceClause()+") TO STDOUT")
+ _, err := conn.PgConn().CopyTo(ctx, w, "COPY (SELECT id, regexp_replace(request::TEXT, '\\\\u0000', '', 'g')::JSON request, code, request_type, creation_date, change_date, instance_id FROM auth.auth_requests "+instanceClause()+" AND change_date > NOW() - INTERVAL '"+strconv.FormatFloat(maxAuthRequestAge.Seconds(), 'f', -1, 64)+" seconds') TO STDOUT")
w.Close()
return err
})
@@ -69,7 +74,7 @@ func copyAuthRequests(ctx context.Context, source, dest *database.DB) {
defer destConn.Close()
var affected int64
- err = destConn.Raw(func(driverConn interface{}) error {
+ err = destConn.Raw(func(driverConn any) error {
conn := driverConn.(*stdlib.Conn).Conn()
if shouldReplace {
diff --git a/cmd/mirror/config.go b/cmd/mirror/config.go
index 9d0113a1d7..5bb19f12de 100644
--- a/cmd/mirror/config.go
+++ b/cmd/mirror/config.go
@@ -23,7 +23,8 @@ type Migration struct {
Source database.Config
Destination database.Config
- EventBulkSize uint32
+ EventBulkSize uint32
+ MaxAuthRequestAge time.Duration
Log *logging.Config
Machine *id.Config
diff --git a/cmd/mirror/defaults.yaml b/cmd/mirror/defaults.yaml
index 4b42c06534..4d8a0a4eae 100644
--- a/cmd/mirror/defaults.yaml
+++ b/cmd/mirror/defaults.yaml
@@ -1,61 +1,64 @@
Source:
cockroach:
- Host: localhost # ZITADEL_DATABASE_COCKROACH_HOST
- Port: 26257 # ZITADEL_DATABASE_COCKROACH_PORT
- Database: zitadel # ZITADEL_DATABASE_COCKROACH_DATABASE
- MaxOpenConns: 6 # ZITADEL_DATABASE_COCKROACH_MAXOPENCONNS
- MaxIdleConns: 6 # ZITADEL_DATABASE_COCKROACH_MAXIDLECONNS
- MaxConnLifetime: 30m # ZITADEL_DATABASE_COCKROACH_MAXCONNLIFETIME
- MaxConnIdleTime: 5m # ZITADEL_DATABASE_COCKROACH_MAXCONNIDLETIME
- Options: "" # ZITADEL_DATABASE_COCKROACH_OPTIONS
+ Host: localhost # ZITADEL_SOURCE_COCKROACH_HOST
+ Port: 26257 # ZITADEL_SOURCE_COCKROACH_PORT
+ Database: zitadel # ZITADEL_SOURCE_COCKROACH_DATABASE
+ MaxOpenConns: 6 # ZITADEL_SOURCE_COCKROACH_MAXOPENCONNS
+ MaxIdleConns: 6 # ZITADEL_SOURCE_COCKROACH_MAXIDLECONNS
+ MaxConnLifetime: 30m # ZITADEL_SOURCE_COCKROACH_MAXCONNLIFETIME
+ MaxConnIdleTime: 5m # ZITADEL_SOURCE_COCKROACH_MAXCONNIDLETIME
+ Options: "" # ZITADEL_SOURCE_COCKROACH_OPTIONS
User:
- Username: zitadel # ZITADEL_DATABASE_COCKROACH_USER_USERNAME
- Password: "" # ZITADEL_DATABASE_COCKROACH_USER_PASSWORD
+ Username: zitadel # ZITADEL_SOURCE_COCKROACH_USER_USERNAME
+ Password: "" # ZITADEL_SOURCE_COCKROACH_USER_PASSWORD
SSL:
- Mode: disable # ZITADEL_DATABASE_COCKROACH_USER_SSL_MODE
- RootCert: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_ROOTCERT
- Cert: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_CERT
- Key: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_KEY
+ Mode: disable # ZITADEL_SOURCE_COCKROACH_USER_SSL_MODE
+ RootCert: "" # ZITADEL_SOURCE_COCKROACH_USER_SSL_ROOTCERT
+ Cert: "" # ZITADEL_SOURCE_COCKROACH_USER_SSL_CERT
+ Key: "" # ZITADEL_SOURCE_COCKROACH_USER_SSL_KEY
# Postgres is used as soon as a value is set
# The values describe the possible fields to set values
postgres:
- Host: # ZITADEL_DATABASE_POSTGRES_HOST
- Port: # ZITADEL_DATABASE_POSTGRES_PORT
- Database: # ZITADEL_DATABASE_POSTGRES_DATABASE
- MaxOpenConns: # ZITADEL_DATABASE_POSTGRES_MAXOPENCONNS
- MaxIdleConns: # ZITADEL_DATABASE_POSTGRES_MAXIDLECONNS
- MaxConnLifetime: # ZITADEL_DATABASE_POSTGRES_MAXCONNLIFETIME
- MaxConnIdleTime: # ZITADEL_DATABASE_POSTGRES_MAXCONNIDLETIME
- Options: # ZITADEL_DATABASE_POSTGRES_OPTIONS
+ Host: # ZITADEL_SOURCE_POSTGRES_HOST
+ Port: # ZITADEL_SOURCE_POSTGRES_PORT
+ Database: # ZITADEL_SOURCE_POSTGRES_DATABASE
+ MaxOpenConns: # ZITADEL_SOURCE_POSTGRES_MAXOPENCONNS
+ MaxIdleConns: # ZITADEL_SOURCE_POSTGRES_MAXIDLECONNS
+ MaxConnLifetime: # ZITADEL_SOURCE_POSTGRES_MAXCONNLIFETIME
+ MaxConnIdleTime: # ZITADEL_SOURCE_POSTGRES_MAXCONNIDLETIME
+ Options: # ZITADEL_SOURCE_POSTGRES_OPTIONS
User:
- Username: # ZITADEL_DATABASE_POSTGRES_USER_USERNAME
- Password: # ZITADEL_DATABASE_POSTGRES_USER_PASSWORD
+ Username: # ZITADEL_SOURCE_POSTGRES_USER_USERNAME
+ Password: # ZITADEL_SOURCE_POSTGRES_USER_PASSWORD
SSL:
- Mode: # ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE
- RootCert: # ZITADEL_DATABASE_POSTGRES_USER_SSL_ROOTCERT
- Cert: # ZITADEL_DATABASE_POSTGRES_USER_SSL_CERT
- Key: # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY
+ Mode: # ZITADEL_SOURCE_POSTGRES_USER_SSL_MODE
+ RootCert: # ZITADEL_SOURCE_POSTGRES_USER_SSL_ROOTCERT
+ Cert: # ZITADEL_SOURCE_POSTGRES_USER_SSL_CERT
+ Key: # ZITADEL_SOURCE_POSTGRES_USER_SSL_KEY
Destination:
postgres:
- Host: localhost # ZITADEL_DATABASE_POSTGRES_HOST
- Port: 5432 # ZITADEL_DATABASE_POSTGRES_PORT
- Database: zitadel # ZITADEL_DATABASE_POSTGRES_DATABASE
- MaxOpenConns: 5 # ZITADEL_DATABASE_POSTGRES_MAXOPENCONNS
- MaxIdleConns: 2 # ZITADEL_DATABASE_POSTGRES_MAXIDLECONNS
- MaxConnLifetime: 30m # ZITADEL_DATABASE_POSTGRES_MAXCONNLIFETIME
- MaxConnIdleTime: 5m # ZITADEL_DATABASE_POSTGRES_MAXCONNIDLETIME
- Options: "" # ZITADEL_DATABASE_POSTGRES_OPTIONS
+ Host: localhost # ZITADEL_DESTINATION_POSTGRES_HOST
+ Port: 5432 # ZITADEL_DESTINATION_POSTGRES_PORT
+ Database: zitadel # ZITADEL_DESTINATION_POSTGRES_DATABASE
+ MaxOpenConns: 5 # ZITADEL_DESTINATION_POSTGRES_MAXOPENCONNS
+ MaxIdleConns: 2 # ZITADEL_DESTINATION_POSTGRES_MAXIDLECONNS
+ MaxConnLifetime: 30m # ZITADEL_DESTINATION_POSTGRES_MAXCONNLIFETIME
+ MaxConnIdleTime: 5m # ZITADEL_DESTINATION_POSTGRES_MAXCONNIDLETIME
+ Options: "" # ZITADEL_DESTINATION_POSTGRES_OPTIONS
User:
- Username: zitadel # ZITADEL_DATABASE_POSTGRES_USER_USERNAME
- Password: "" # ZITADEL_DATABASE_POSTGRES_USER_PASSWORD
+ Username: zitadel # ZITADEL_DESTINATION_POSTGRES_USER_USERNAME
+ Password: "" # ZITADEL_DESTINATION_POSTGRES_USER_PASSWORD
SSL:
- Mode: disable # ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE
- RootCert: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_ROOTCERT
- Cert: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_CERT
- Key: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY
+ Mode: disable # ZITADEL_DESTINATION_POSTGRES_USER_SSL_MODE
+ RootCert: "" # ZITADEL_DESTINATION_POSTGRES_USER_SSL_ROOTCERT
+ Cert: "" # ZITADEL_DESTINATION_POSTGRES_USER_SSL_CERT
+ Key: "" # ZITADEL_DESTINATION_POSTGRES_USER_SSL_KEY
-EventBulkSize: 10000
+EventBulkSize: 10000 # ZITADEL_EVENTBULKSIZE
+# The maximum duration an auth request was last updated before it gets ignored.
+# Default is 30 days
+MaxAuthRequestAge: 720h # ZITADEL_MAXAUTHREQUESTAGE
Projections:
# The maximum duration a transaction remains open
@@ -64,14 +67,14 @@ Projections:
TransactionDuration: 0s # ZITADEL_PROJECTIONS_TRANSACTIONDURATION
# turn off scheduler during operation
RequeueEvery: 0s
- ConcurrentInstances: 7
- EventBulkLimit: 1000
- Customizations:
+ ConcurrentInstances: 7 # ZITADEL_PROJECTIONS_CONCURRENTINSTANCES
+ EventBulkLimit: 1000 # ZITADEL_PROJECTIONS_EVENTBULKLIMIT
+ Customizations:
notifications:
MaxFailureCount: 1
Eventstore:
- MaxRetries: 3
+ MaxRetries: 3 # ZITADEL_EVENTSTORE_MAXRETRIES
Auth:
Spooler:
diff --git a/cmd/mirror/event_store.go b/cmd/mirror/event_store.go
index 8ce53b150a..41c529c025 100644
--- a/cmd/mirror/event_store.go
+++ b/cmd/mirror/event_store.go
@@ -69,6 +69,7 @@ func positionQuery(db *db.DB) string {
}
func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) {
+ logging.Info("starting to copy events")
start := time.Now()
reader, writer := io.Pipe()
@@ -130,7 +131,10 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) {
if err != nil {
return zerrors.ThrowUnknownf(err, "MIGRA-KTuSq", "unable to copy events from source during iteration %d", i)
}
+ logging.WithFields("batch_count", i).Info("batch of events copied")
+
if tag.RowsAffected() < int64(bulkSize) {
+ logging.WithFields("batch_count", i).Info("last batch of events copied")
return nil
}
@@ -202,6 +206,7 @@ func writeCopyEventsDone(ctx context.Context, es *eventstore.EventStore, id, sou
}
func copyUniqueConstraints(ctx context.Context, source, dest *db.DB) {
+ logging.Info("starting to copy unique constraints")
start := time.Now()
reader, writer := io.Pipe()
errs := make(chan error, 1)
diff --git a/cmd/mirror/projections.go b/cmd/mirror/projections.go
index 66b3fb1a26..4e12b29748 100644
--- a/cmd/mirror/projections.go
+++ b/cmd/mirror/projections.go
@@ -3,6 +3,7 @@ package mirror
import (
"context"
"database/sql"
+ "fmt"
"net/http"
"sync"
"time"
@@ -104,6 +105,7 @@ func projections(
config *ProjectionsConfig,
masterKey string,
) {
+ logging.Info("starting to fill projections")
start := time.Now()
client, err := database.Connect(config.Destination, false)
@@ -255,8 +257,10 @@ func projections(
go execProjections(ctx, instances, failedInstances, &wg)
}
- for _, instance := range queryInstanceIDs(ctx, client) {
+ existingInstances := queryInstanceIDs(ctx, client)
+ for i, instance := range existingInstances {
instances <- instance
+ logging.WithFields("id", instance, "index", fmt.Sprintf("%d/%d", i, len(existingInstances))).Info("instance queued for projection")
}
close(instances)
wg.Wait()
@@ -268,7 +272,7 @@ func projections(
func execProjections(ctx context.Context, instances <-chan string, failedInstances chan<- string, wg *sync.WaitGroup) {
for instance := range instances {
- logging.WithFields("instance", instance).Info("start projections")
+ logging.WithFields("instance", instance).Info("starting projections")
ctx = internal_authz.WithInstanceID(ctx, instance)
err := projection.ProjectInstance(ctx)
@@ -311,7 +315,7 @@ func execProjections(ctx context.Context, instances <-chan string, failedInstanc
wg.Done()
}
-// returns the instance configured by flag
+// queryInstanceIDs returns the instance configured by flag
// or all instances which are not removed
func queryInstanceIDs(ctx context.Context, source *database.DB) []string {
if len(instanceIDs) > 0 {
diff --git a/cmd/mirror/system.go b/cmd/mirror/system.go
index 00b48eb491..57eb205436 100644
--- a/cmd/mirror/system.go
+++ b/cmd/mirror/system.go
@@ -46,6 +46,7 @@ func copySystem(ctx context.Context, config *Migration) {
}
func copyAssets(ctx context.Context, source, dest *database.DB) {
+ logging.Info("starting to copy assets")
start := time.Now()
sourceConn, err := source.Conn(ctx)
@@ -70,7 +71,7 @@ func copyAssets(ctx context.Context, source, dest *database.DB) {
logging.OnError(err).Fatal("unable to acquire dest connection")
defer destConn.Close()
- var eventCount int64
+ var assetCount int64
err = destConn.Raw(func(driverConn interface{}) error {
conn := driverConn.(*stdlib.Conn).Conn()
@@ -82,16 +83,17 @@ func copyAssets(ctx context.Context, source, dest *database.DB) {
}
tag, err := conn.PgConn().CopyFrom(ctx, r, "COPY system.assets (instance_id, asset_type, resource_owner, name, content_type, data, updated_at) FROM stdin")
- eventCount = tag.RowsAffected()
+ assetCount = tag.RowsAffected()
return err
})
logging.OnError(err).Fatal("unable to copy assets to destination")
logging.OnError(<-errs).Fatal("unable to copy assets from source")
- logging.WithFields("took", time.Since(start), "count", eventCount).Info("assets migrated")
+ logging.WithFields("took", time.Since(start), "count", assetCount).Info("assets migrated")
}
func copyEncryptionKeys(ctx context.Context, source, dest *database.DB) {
+ logging.Info("starting to copy encryption keys")
start := time.Now()
sourceConn, err := source.Conn(ctx)
@@ -116,7 +118,7 @@ func copyEncryptionKeys(ctx context.Context, source, dest *database.DB) {
logging.OnError(err).Fatal("unable to acquire dest connection")
defer destConn.Close()
- var eventCount int64
+ var keyCount int64
err = destConn.Raw(func(driverConn interface{}) error {
conn := driverConn.(*stdlib.Conn).Conn()
@@ -128,11 +130,11 @@ func copyEncryptionKeys(ctx context.Context, source, dest *database.DB) {
}
tag, err := conn.PgConn().CopyFrom(ctx, r, "COPY system.encryption_keys FROM stdin")
- eventCount = tag.RowsAffected()
+ keyCount = tag.RowsAffected()
return err
})
logging.OnError(err).Fatal("unable to copy encryption keys to destination")
logging.OnError(<-errs).Fatal("unable to copy encryption keys from source")
- logging.WithFields("took", time.Since(start), "count", eventCount).Info("encryption keys migrated")
+ logging.WithFields("took", time.Since(start), "count", keyCount).Info("encryption keys migrated")
}
diff --git a/docs/docs/self-hosting/manage/cli/mirror.mdx b/docs/docs/self-hosting/manage/cli/mirror.mdx
index 45bac9b279..ae81800e39 100644
--- a/docs/docs/self-hosting/manage/cli/mirror.mdx
+++ b/docs/docs/self-hosting/manage/cli/mirror.mdx
@@ -158,6 +158,9 @@ Destination:
# As cockroachdb first copies the data into memory this parameter is used to iterate through the events table and fetch only the given amount of events per iteration
EventBulkSize: 10000 # ZITADEL_EVENTBULKSIZE
+# The maximum duration an auth request was last updated before it gets ignored.
+# Default is 30 days
+MaxAuthRequestAge: 720h # ZITADEL_MAXAUTHREQUESTAGE
Projections:
# Defines how many projections are allowed to run in parallel
diff --git a/internal/admin/repository/eventsourcing/handler/handler.go b/internal/admin/repository/eventsourcing/handler/handler.go
index ec268c25a1..76584b55b0 100644
--- a/internal/admin/repository/eventsourcing/handler/handler.go
+++ b/internal/admin/repository/eventsourcing/handler/handler.go
@@ -2,9 +2,13 @@ package handler
import (
"context"
+ "fmt"
"time"
+ "github.com/zitadel/logging"
+
"github.com/zitadel/zitadel/internal/admin/repository/eventsourcing/view"
+ "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
@@ -57,11 +61,13 @@ func Start(ctx context.Context) {
}
func ProjectInstance(ctx context.Context) error {
- for _, projection := range projections {
+ for i, projection := range projections {
+ logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("starting admin projection")
_, err := projection.Trigger(ctx)
if err != nil {
return err
}
+ logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("admin projection done")
}
return nil
}
diff --git a/internal/auth/repository/eventsourcing/handler/handler.go b/internal/auth/repository/eventsourcing/handler/handler.go
index 0d87ab06bb..74a27a8312 100644
--- a/internal/auth/repository/eventsourcing/handler/handler.go
+++ b/internal/auth/repository/eventsourcing/handler/handler.go
@@ -2,8 +2,12 @@ package handler
import (
"context"
+ "fmt"
"time"
+ "github.com/zitadel/logging"
+
+ "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/auth/repository/eventsourcing/view"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore"
@@ -72,11 +76,13 @@ func Projections() []*handler2.Handler {
}
func ProjectInstance(ctx context.Context) error {
- for _, projection := range projections {
+ for i, projection := range projections {
+ logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("starting auth projection")
_, err := projection.Trigger(ctx)
if err != nil {
return err
}
+ logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("auth projection done")
}
return nil
}
diff --git a/internal/notification/projections.go b/internal/notification/projections.go
index a2d4d4140e..9b6b975fa1 100644
--- a/internal/notification/projections.go
+++ b/internal/notification/projections.go
@@ -2,8 +2,12 @@ package notification
import (
"context"
+ "fmt"
"time"
+ "github.com/zitadel/logging"
+
+ "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/eventstore"
@@ -68,11 +72,13 @@ func Start(ctx context.Context) {
}
func ProjectInstance(ctx context.Context) error {
- for _, projection := range projections {
+ for i, projection := range projections {
+ logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("starting notification projection")
_, err := projection.Trigger(ctx)
if err != nil {
return err
}
+ logging.WithFields("name", projection.ProjectionName(), "instance", authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("notification projection done")
}
return nil
}
diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go
index f4e3bbe0d4..07953a27e8 100644
--- a/internal/query/projection/projection.go
+++ b/internal/query/projection/projection.go
@@ -2,6 +2,9 @@ package projection
import (
"context"
+ "fmt"
+
+ "github.com/zitadel/logging"
internal_authz "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
@@ -90,6 +93,7 @@ var (
)
type projection interface {
+ ProjectionName() string
Start(ctx context.Context)
Init(ctx context.Context) error
Trigger(ctx context.Context, opts ...handler.TriggerOpt) (_ context.Context, err error)
@@ -206,21 +210,25 @@ func Start(ctx context.Context) {
}
func ProjectInstance(ctx context.Context) error {
- for _, projection := range projections {
+ for i, projection := range projections {
+ logging.WithFields("name", projection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(projections))).Info("starting projection")
_, err := projection.Trigger(ctx)
if err != nil {
return err
}
+ logging.WithFields("name", projection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID()).Info("projection done")
}
return nil
}
func ProjectInstanceFields(ctx context.Context) error {
- for _, fieldProjection := range fields {
+ for i, fieldProjection := range fields {
+ logging.WithFields("name", fieldProjection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID(), "index", fmt.Sprintf("%d/%d", i, len(fields))).Info("starting fields projection")
err := fieldProjection.Trigger(ctx)
if err != nil {
return err
}
+ logging.WithFields("name", fieldProjection.ProjectionName(), "instance", internal_authz.GetInstance(ctx).InstanceID()).Info("fields projection done")
}
return nil
}
From 0465d5093ef009e9bbea6998ca383bcb20144136 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tim=20M=C3=B6hlmann?=
Date: Wed, 30 Apr 2025 10:26:04 +0200
Subject: [PATCH 029/181] fix(features): remove the improved performance enumer
(#9819)
# Which Problems Are Solved
Instance that had improved performance flags set, got event errors when
getting instance features. This is because the improved performance
flags were marshalled using the enumerated integers, but now needed to
be unmashalled using the added UnmarshallText method.
# How the Problems Are Solved
- Remove emnumer generation
# Additional Changes
- none
# Additional Context
- reported on QA
- Backport to next-rc / v3
---
cmd/defaults.yaml | 13 ++-
internal/feature/feature.go | 3 +-
.../feature/improvedperformancetype_enumer.go | 106 ------------------
3 files changed, 9 insertions(+), 113 deletions(-)
delete mode 100644 internal/feature/improvedperformancetype_enumer.go
diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml
index f20fbc03fc..6ab01ab35b 100644
--- a/cmd/defaults.yaml
+++ b/cmd/defaults.yaml
@@ -1102,12 +1102,13 @@ DefaultInstance:
# LegacyIntrospection: false # ZITADEL_DEFAULTINSTANCE_FEATURES_LEGACYINTROSPECTION
# UserSchema: false # ZITADEL_DEFAULTINSTANCE_FEATURES_USERSCHEMA
# TokenExchange: false # ZITADEL_DEFAULTINSTANCE_FEATURES_TOKENEXCHANGE
- # ImprovedPerformance: # ZITADEL_DEFAULTINSTANCE_FEATURES_IMPROVEDPERFORMANCE
- # - OrgByID
- # - ProjectGrant
- # - Project
- # - UserGrant
- # - OrgDomainVerified
+ ImprovedPerformance: # ZITADEL_DEFAULTINSTANCE_FEATURES_IMPROVEDPERFORMANCE
+ # https://github.com/zitadel/zitadel/blob/main/internal/feature/feature.go#L64-L68
+ # - 1 # OrgByID
+ # - 2 # ProjectGrant
+ # - 3 # Project
+ # - 4 # UserGrant
+ # - 5 # OrgDomainVerified
# WebKey: false # ZITADEL_DEFAULTINSTANCE_FEATURES_WEBKEY
# DebugOIDCParentError: false # ZITADEL_DEFAULTINSTANCE_FEATURES_DEBUGOIDCPARENTERROR
# OIDCSingleV1SessionTermination: false # ZITADEL_DEFAULTINSTANCE_FEATURES_OIDCSINGLEV1SESSIONTERMINATION
diff --git a/internal/feature/feature.go b/internal/feature/feature.go
index f500b80eb3..b5f5a901d4 100644
--- a/internal/feature/feature.go
+++ b/internal/feature/feature.go
@@ -57,7 +57,8 @@ type Features struct {
ConsoleUseV2UserApi bool `json:"console_use_v2_user_api,omitempty"`
}
-//go:generate enumer -type ImprovedPerformanceType -trimprefix ImprovedPerformanceType -text
+/* Note: do not generate the stringer or enumer for this type, is it breaks existing events */
+
type ImprovedPerformanceType int32
const (
diff --git a/internal/feature/improvedperformancetype_enumer.go b/internal/feature/improvedperformancetype_enumer.go
deleted file mode 100644
index a12673c205..0000000000
--- a/internal/feature/improvedperformancetype_enumer.go
+++ /dev/null
@@ -1,106 +0,0 @@
-// Code generated by "enumer -type ImprovedPerformanceType -trimprefix ImprovedPerformanceType -text"; DO NOT EDIT.
-
-package feature
-
-import (
- "fmt"
- "strings"
-)
-
-const _ImprovedPerformanceTypeName = "UnspecifiedOrgByIDProjectGrantProjectUserGrantOrgDomainVerified"
-
-var _ImprovedPerformanceTypeIndex = [...]uint8{0, 11, 18, 30, 37, 46, 63}
-
-const _ImprovedPerformanceTypeLowerName = "unspecifiedorgbyidprojectgrantprojectusergrantorgdomainverified"
-
-func (i ImprovedPerformanceType) String() string {
- if i < 0 || i >= ImprovedPerformanceType(len(_ImprovedPerformanceTypeIndex)-1) {
- return fmt.Sprintf("ImprovedPerformanceType(%d)", i)
- }
- return _ImprovedPerformanceTypeName[_ImprovedPerformanceTypeIndex[i]:_ImprovedPerformanceTypeIndex[i+1]]
-}
-
-// An "invalid array index" compiler error signifies that the constant values have changed.
-// Re-run the stringer command to generate them again.
-func _ImprovedPerformanceTypeNoOp() {
- var x [1]struct{}
- _ = x[ImprovedPerformanceTypeUnspecified-(0)]
- _ = x[ImprovedPerformanceTypeOrgByID-(1)]
- _ = x[ImprovedPerformanceTypeProjectGrant-(2)]
- _ = x[ImprovedPerformanceTypeProject-(3)]
- _ = x[ImprovedPerformanceTypeUserGrant-(4)]
- _ = x[ImprovedPerformanceTypeOrgDomainVerified-(5)]
-}
-
-var _ImprovedPerformanceTypeValues = []ImprovedPerformanceType{ImprovedPerformanceTypeUnspecified, ImprovedPerformanceTypeOrgByID, ImprovedPerformanceTypeProjectGrant, ImprovedPerformanceTypeProject, ImprovedPerformanceTypeUserGrant, ImprovedPerformanceTypeOrgDomainVerified}
-
-var _ImprovedPerformanceTypeNameToValueMap = map[string]ImprovedPerformanceType{
- _ImprovedPerformanceTypeName[0:11]: ImprovedPerformanceTypeUnspecified,
- _ImprovedPerformanceTypeLowerName[0:11]: ImprovedPerformanceTypeUnspecified,
- _ImprovedPerformanceTypeName[11:18]: ImprovedPerformanceTypeOrgByID,
- _ImprovedPerformanceTypeLowerName[11:18]: ImprovedPerformanceTypeOrgByID,
- _ImprovedPerformanceTypeName[18:30]: ImprovedPerformanceTypeProjectGrant,
- _ImprovedPerformanceTypeLowerName[18:30]: ImprovedPerformanceTypeProjectGrant,
- _ImprovedPerformanceTypeName[30:37]: ImprovedPerformanceTypeProject,
- _ImprovedPerformanceTypeLowerName[30:37]: ImprovedPerformanceTypeProject,
- _ImprovedPerformanceTypeName[37:46]: ImprovedPerformanceTypeUserGrant,
- _ImprovedPerformanceTypeLowerName[37:46]: ImprovedPerformanceTypeUserGrant,
- _ImprovedPerformanceTypeName[46:63]: ImprovedPerformanceTypeOrgDomainVerified,
- _ImprovedPerformanceTypeLowerName[46:63]: ImprovedPerformanceTypeOrgDomainVerified,
-}
-
-var _ImprovedPerformanceTypeNames = []string{
- _ImprovedPerformanceTypeName[0:11],
- _ImprovedPerformanceTypeName[11:18],
- _ImprovedPerformanceTypeName[18:30],
- _ImprovedPerformanceTypeName[30:37],
- _ImprovedPerformanceTypeName[37:46],
- _ImprovedPerformanceTypeName[46:63],
-}
-
-// ImprovedPerformanceTypeString retrieves an enum value from the enum constants string name.
-// Throws an error if the param is not part of the enum.
-func ImprovedPerformanceTypeString(s string) (ImprovedPerformanceType, error) {
- if val, ok := _ImprovedPerformanceTypeNameToValueMap[s]; ok {
- return val, nil
- }
-
- if val, ok := _ImprovedPerformanceTypeNameToValueMap[strings.ToLower(s)]; ok {
- return val, nil
- }
- return 0, fmt.Errorf("%s does not belong to ImprovedPerformanceType values", s)
-}
-
-// ImprovedPerformanceTypeValues returns all values of the enum
-func ImprovedPerformanceTypeValues() []ImprovedPerformanceType {
- return _ImprovedPerformanceTypeValues
-}
-
-// ImprovedPerformanceTypeStrings returns a slice of all String values of the enum
-func ImprovedPerformanceTypeStrings() []string {
- strs := make([]string, len(_ImprovedPerformanceTypeNames))
- copy(strs, _ImprovedPerformanceTypeNames)
- return strs
-}
-
-// IsAImprovedPerformanceType returns "true" if the value is listed in the enum definition. "false" otherwise
-func (i ImprovedPerformanceType) IsAImprovedPerformanceType() bool {
- for _, v := range _ImprovedPerformanceTypeValues {
- if i == v {
- return true
- }
- }
- return false
-}
-
-// MarshalText implements the encoding.TextMarshaler interface for ImprovedPerformanceType
-func (i ImprovedPerformanceType) MarshalText() ([]byte, error) {
- return []byte(i.String()), nil
-}
-
-// UnmarshalText implements the encoding.TextUnmarshaler interface for ImprovedPerformanceType
-func (i *ImprovedPerformanceType) UnmarshalText(text []byte) error {
- var err error
- *i, err = ImprovedPerformanceTypeString(string(text))
- return err
-}
From 3953879fe9c2533289dab8a67a5e1a0514500150 Mon Sep 17 00:00:00 2001
From: Stefan Benz <46600784+stebenz@users.noreply.github.com>
Date: Wed, 30 Apr 2025 11:12:48 +0200
Subject: [PATCH 030/181] fix: correct unmarshalling of IdP user when using
Google (#9799)
# Which Problems Are Solved
Users from Google IDP's are not unmarshalled correctly in intent
endpoints and not returned to callers.
# How the Problems Are Solved
Provided correct type for unmarshalling of the information.
# Additional Changes
None
# Additional Context
None
---------
Co-authored-by: Livio Spring
---
internal/api/grpc/user/v2/intent.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/internal/api/grpc/user/v2/intent.go b/internal/api/grpc/user/v2/intent.go
index 6e46dfd5c3..06966edb35 100644
--- a/internal/api/grpc/user/v2/intent.go
+++ b/internal/api/grpc/user/v2/intent.go
@@ -182,7 +182,7 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.R
case *gitlab.Provider:
idpUser, err = unmarshalIdpUser(intent.IDPUser, &oidc.User{UserInfo: &oidc_pkg.UserInfo{}})
case *google.Provider:
- idpUser, err = unmarshalIdpUser(intent.IDPUser, &oidc.User{UserInfo: &oidc_pkg.UserInfo{}})
+ idpUser, err = unmarshalIdpUser(intent.IDPUser, &google.User{User: &oidc.User{UserInfo: &oidc_pkg.UserInfo{}}})
case *saml.Provider:
idpUser, err = unmarshalIdpUser(intent.IDPUser, &saml.UserMapper{})
case *ldap.Provider:
From 002c3eb025693b51b99670d05cd50953b4c8ca48 Mon Sep 17 00:00:00 2001
From: Ramon
Date: Wed, 30 Apr 2025 13:16:44 +0200
Subject: [PATCH 031/181] fix: Use ID ordering for the executions in Actions v2
(#9820)
# Which Problems Are Solved
Sort Executions by ID in the Actions V2 view. This way All is the first
element in the table.
# How the Problems Are Solved
Pass ID sorting to the Backend.
# Additional Changes
Cleaned up some imports.
# Additional Context
- Part of Make actions sortable by hirarchie #9688
---
.../actions-two-actions/actions-two-actions.component.ts | 3 ++-
console/src/app/services/grpc.service.ts | 5 -----
2 files changed, 2 insertions(+), 6 deletions(-)
diff --git a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.ts b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.ts
index b5f2260e34..7e0d457dd5 100644
--- a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.ts
+++ b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions.component.ts
@@ -16,6 +16,7 @@ import { MessageInitShape } from '@bufbuild/protobuf';
import { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
import { InfoSectionType } from '../../info-section/info-section.component';
+import { ExecutionFieldName } from '@zitadel/proto/zitadel/action/v2beta/query_pb';
@Component({
selector: 'cnsl-actions-two-actions',
@@ -42,7 +43,7 @@ export class ActionsTwoActionsComponent {
return this.refresh$.pipe(
startWith(true),
switchMap(() => {
- return this.actionService.listExecutions({});
+ return this.actionService.listExecutions({ sortingColumn: ExecutionFieldName.ID, pagination: { asc: true } });
}),
map(({ result }) => result.map(correctlyTypeExecution)),
catchError((err) => {
diff --git a/console/src/app/services/grpc.service.ts b/console/src/app/services/grpc.service.ts
index b2f89ca648..d2add12f41 100644
--- a/console/src/app/services/grpc.service.ts
+++ b/console/src/app/services/grpc.service.ts
@@ -15,7 +15,6 @@ import { AuthInterceptor, AuthInterceptorProvider, NewConnectWebAuthInterceptor
import { ExhaustedGrpcInterceptor } from './interceptors/exhausted.grpc.interceptor';
import { I18nInterceptor } from './interceptors/i18n.interceptor';
import { NewConnectWebOrgInterceptor, OrgInterceptor, OrgInterceptorProvider } from './interceptors/org.interceptor';
-import { StorageService } from './storage.service';
import { UserServiceClient } from '../proto/generated/zitadel/user/v2/User_serviceServiceClientPb';
//@ts-ignore
import { createFeatureServiceClient, createUserServiceClient, createSessionServiceClient } from '@zitadel/client/v2';
@@ -24,14 +23,10 @@ import { createAuthServiceClient, createManagementServiceClient } from '@zitadel
import { createGrpcWebTransport } from '@connectrpc/connect-web';
// @ts-ignore
import { createClientFor } from '@zitadel/client';
-import { Client, Transport } from '@connectrpc/connect';
import { WebKeyService } from '@zitadel/proto/zitadel/webkey/v2beta/webkey_service_pb';
import { ActionService } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
-// @ts-ignore
-import { createClientFor } from '@zitadel/client';
-
const createWebKeyServiceClient = createClientFor(WebKeyService);
const createActionServiceClient = createClientFor(ActionService);
From 48c1f7e49f47e507e4c31cfc84f8a3a043278969 Mon Sep 17 00:00:00 2001
From: Ramon
Date: Wed, 30 Apr 2025 14:22:27 +0200
Subject: [PATCH 032/181] fix: Actions V2 improve deleted target handling in
executions (#9822)
# Which Problems Are Solved
Previously, if a target was deleted but still referenced by an
execution, it became impossible to load the executions.
# How the Problems Are Solved
Missing targets in the execution table are now gracefully ignored,
allowing executions to load without errors.
# Additional Changes
Enhanced permission handling in the settings sidenav to ensure users
have the correct access rights.
---
.../actions-two-actions-table.component.html | 4 ++--
.../actions-two-actions-table.component.ts | 10 +++-------
console/src/app/modules/settings-list/settings.ts | 6 ++----
console/src/app/services/grpc-auth.service.ts | 15 +++++++++++++--
4 files changed, 20 insertions(+), 15 deletions(-)
diff --git a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.html b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.html
index 82f04fb124..7948ba7554 100644
--- a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.html
+++ b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.html
@@ -24,8 +24,8 @@
{{ 'ACTIONSTWO.EXECUTION.TABLE.TARGET' | translate }}
- {{ target.name }}
+
+ {{ target.name }}
diff --git a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.ts b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.ts
index 658c205c4e..af9673dbf5 100644
--- a/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.ts
+++ b/console/src/app/modules/actions-two/actions-two-actions/actions-two-actions-table/actions-two-actions-table.component.ts
@@ -55,13 +55,9 @@ export class ActionsTwoActionsTableComponent {
}
return executions.map((execution) => {
- const mappedTargets = execution.targets.map((target) => {
- const targetType = targetsMap.get(target);
- if (!targetType) {
- throw new Error(`Target with id ${target} not found`);
- }
- return targetType;
- });
+ const mappedTargets = execution.targets
+ .map((target) => targetsMap.get(target))
+ .filter((target): target is NonNullable => !!target);
return { execution, mappedTargets };
});
});
diff --git a/console/src/app/modules/settings-list/settings.ts b/console/src/app/modules/settings-list/settings.ts
index c96431fa30..7ec7fdea15 100644
--- a/console/src/app/modules/settings-list/settings.ts
+++ b/console/src/app/modules/settings-list/settings.ts
@@ -228,8 +228,7 @@ export const ACTIONS: SidenavSetting = {
i18nKey: 'SETTINGS.LIST.ACTIONS',
groupI18nKey: 'SETTINGS.GROUPS.ACTIONS',
requiredRoles: {
- // todo: figure out roles
- [PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
+ [PolicyComponentServiceType.ADMIN]: ['action.execution.write', 'action.target.write'],
},
beta: true,
};
@@ -239,8 +238,7 @@ export const ACTIONS_TARGETS: SidenavSetting = {
i18nKey: 'SETTINGS.LIST.TARGETS',
groupI18nKey: 'SETTINGS.GROUPS.ACTIONS',
requiredRoles: {
- // todo: figure out roles
- [PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
+ [PolicyComponentServiceType.ADMIN]: ['action.execution.write', 'action.target.write'],
},
beta: true,
};
diff --git a/console/src/app/services/grpc-auth.service.ts b/console/src/app/services/grpc-auth.service.ts
index 3967f1df06..198d048b6a 100644
--- a/console/src/app/services/grpc-auth.service.ts
+++ b/console/src/app/services/grpc-auth.service.ts
@@ -1,7 +1,18 @@
import { Injectable } from '@angular/core';
import { SortDirection } from '@angular/material/sort';
import { OAuthService } from 'angular-oauth2-oidc';
-import { BehaviorSubject, combineLatestWith, EMPTY, mergeWith, NEVER, Observable, of, shareReplay, Subject } from 'rxjs';
+import {
+ BehaviorSubject,
+ combineLatestWith,
+ EMPTY,
+ identity,
+ mergeWith,
+ NEVER,
+ Observable,
+ of,
+ shareReplay,
+ Subject,
+} from 'rxjs';
import { catchError, distinctUntilChanged, filter, finalize, map, startWith, switchMap, tap, timeout } from 'rxjs/operators';
import {
@@ -326,7 +337,7 @@ export class GrpcAuthService {
return new RegExp(reqRegexp).test(role);
});
- const allCheck = requestedRoles.map(test).every((x) => !!x);
+ const allCheck = requestedRoles.map(test).every(identity);
const oneCheck = requestedRoles.some(test);
return requiresAll ? allCheck : oneCheck;
From a05f7ce3fc864358ce3ef5cdd93386162eac5b25 Mon Sep 17 00:00:00 2001
From: Stefan Benz <46600784+stebenz@users.noreply.github.com>
Date: Wed, 30 Apr 2025 14:58:10 +0200
Subject: [PATCH 033/181] fix: correct handling of removed targets (#9824)
# Which Problems Are Solved
In Actions v2, if a target is removed, which is still used in an
execution, the target is still listed when list executions.
# How the Problems Are Solved
Removed targets are now also removed from the executions.
# Additional Changes
To be sure the list executions include a check if the target is still
existing.
# Additional Context
None
Co-authored-by: Livio Spring
---
internal/query/execution.go | 12 ++--
internal/query/execution_targets.sql | 22 ++++---
internal/query/execution_test.go | 71 +++++++++++++++++++--
internal/query/projection/execution.go | 25 ++++++++
internal/query/projection/execution_test.go | 30 +++++++++
5 files changed, 141 insertions(+), 19 deletions(-)
diff --git a/internal/query/execution.go b/internal/query/execution.go
index 0a2a989918..4739a5839e 100644
--- a/internal/query/execution.go
+++ b/internal/query/execution.go
@@ -1,11 +1,13 @@
package query
import (
+ "cmp"
"context"
"database/sql"
_ "embed"
"encoding/json"
"errors"
+ "slices"
"time"
sq "github.com/Masterminds/squirrel"
@@ -301,13 +303,15 @@ func executionTargetsUnmarshal(data []byte) ([]*exec.Target, error) {
}
targets := make([]*exec.Target, len(executionTargets))
- // position starts with 1
- for _, item := range executionTargets {
+ slices.SortFunc(executionTargets, func(a, b *executionTarget) int {
+ return cmp.Compare(a.Position, b.Position)
+ })
+ for i, item := range executionTargets {
if item.Target != "" {
- targets[item.Position-1] = &exec.Target{Type: domain.ExecutionTargetTypeTarget, Target: item.Target}
+ targets[i] = &exec.Target{Type: domain.ExecutionTargetTypeTarget, Target: item.Target}
}
if item.Include != "" {
- targets[item.Position-1] = &exec.Target{Type: domain.ExecutionTargetTypeInclude, Target: item.Include}
+ targets[i] = &exec.Target{Type: domain.ExecutionTargetTypeInclude, Target: item.Include}
}
}
return targets, nil
diff --git a/internal/query/execution_targets.sql b/internal/query/execution_targets.sql
index 32257f4a1f..a6e6dd6caa 100644
--- a/internal/query/execution_targets.sql
+++ b/internal/query/execution_targets.sql
@@ -1,11 +1,15 @@
-SELECT instance_id,
- execution_id,
+SELECT et.instance_id,
+ et.execution_id,
JSONB_AGG(
JSON_OBJECT(
- 'position' : position,
- 'include' : include,
- 'target' : target_id
- )
- ) as targets
-FROM projections.executions1_targets
-GROUP BY instance_id, execution_id
\ No newline at end of file
+ 'position' : et.position,
+ 'include' : et.include,
+ 'target' : et.target_id
+ )
+ ) as targets
+FROM projections.executions1_targets AS et
+ INNER JOIN projections.targets2 AS t
+ ON et.instance_id = t.instance_id
+ AND et.target_id IS NOT NULL
+ AND et.target_id = t.id
+GROUP BY et.instance_id, et.execution_id
\ No newline at end of file
diff --git a/internal/query/execution_test.go b/internal/query/execution_test.go
index eaaac1e9ba..64f9a4849f 100644
--- a/internal/query/execution_test.go
+++ b/internal/query/execution_test.go
@@ -22,9 +22,10 @@ var (
` COUNT(*) OVER ()` +
` FROM projections.executions1` +
` JOIN (` +
- `SELECT instance_id, execution_id, JSONB_AGG( JSON_OBJECT( 'position' : position, 'include' : include, 'target' : target_id ) ) as targets` +
- ` FROM projections.executions1_targets` +
- ` GROUP BY instance_id, execution_id` +
+ `SELECT et.instance_id, et.execution_id, JSONB_AGG( JSON_OBJECT( 'position' : et.position, 'include' : et.include, 'target' : et.target_id ) ) as targets` +
+ ` FROM projections.executions1_targets AS et` +
+ ` INNER JOIN projections.targets2 AS t ON et.instance_id = t.instance_id AND et.target_id IS NOT NULL AND et.target_id = t.id` +
+ ` GROUP BY et.instance_id, et.execution_id` +
`)` +
` AS execution_targets` +
` ON execution_targets.instance_id = projections.executions1.instance_id` +
@@ -45,9 +46,10 @@ var (
` execution_targets.targets` +
` FROM projections.executions1` +
` JOIN (` +
- `SELECT instance_id, execution_id, JSONB_AGG( JSON_OBJECT( 'position' : position, 'include' : include, 'target' : target_id ) ) as targets` +
- ` FROM projections.executions1_targets` +
- ` GROUP BY instance_id, execution_id` +
+ `SELECT et.instance_id, et.execution_id, JSONB_AGG( JSON_OBJECT( 'position' : et.position, 'include' : et.include, 'target' : et.target_id ) ) as targets` +
+ ` FROM projections.executions1_targets AS et` +
+ ` INNER JOIN projections.targets2 AS t ON et.instance_id = t.instance_id AND et.target_id IS NOT NULL AND et.target_id = t.id` +
+ ` GROUP BY et.instance_id, et.execution_id` +
`)` +
` AS execution_targets` +
` ON execution_targets.instance_id = projections.executions1.instance_id` +
@@ -179,6 +181,63 @@ func Test_ExecutionPrepares(t *testing.T) {
},
},
},
+ {
+ name: "prepareExecutionsQuery multiple result, removed target, position missing",
+ prepare: prepareExecutionsQuery,
+ want: want{
+ sqlExpectations: mockQueries(
+ regexp.QuoteMeta(prepareExecutionsStmt),
+ prepareExecutionsCols,
+ [][]driver.Value{
+ {
+ "ro",
+ "id-1",
+ testNow,
+ testNow,
+ []byte(`[{"position" : 1, "target" : "target"}, {"position" : 3, "include" : "include"}]`),
+ },
+ {
+ "ro",
+ "id-2",
+ testNow,
+ testNow,
+ []byte(`[{"position" : 2, "target" : "target"}, {"position" : 1, "include" : "include"}]`),
+ },
+ },
+ ),
+ },
+ object: &Executions{
+ SearchResponse: SearchResponse{
+ Count: 2,
+ },
+ Executions: []*Execution{
+ {
+ ObjectDetails: domain.ObjectDetails{
+ ID: "id-1",
+ EventDate: testNow,
+ CreationDate: testNow,
+ ResourceOwner: "ro",
+ },
+ Targets: []*exec.Target{
+ {Type: domain.ExecutionTargetTypeTarget, Target: "target"},
+ {Type: domain.ExecutionTargetTypeInclude, Target: "include"},
+ },
+ },
+ {
+ ObjectDetails: domain.ObjectDetails{
+ ID: "id-2",
+ EventDate: testNow,
+ CreationDate: testNow,
+ ResourceOwner: "ro",
+ },
+ Targets: []*exec.Target{
+ {Type: domain.ExecutionTargetTypeInclude, Target: "include"},
+ {Type: domain.ExecutionTargetTypeTarget, Target: "target"},
+ },
+ },
+ },
+ },
+ },
{
name: "prepareExecutionsQuery sql err",
prepare: prepareExecutionsQuery,
diff --git a/internal/query/projection/execution.go b/internal/query/projection/execution.go
index 9001fcd3ba..1bd7f2e7f5 100644
--- a/internal/query/projection/execution.go
+++ b/internal/query/projection/execution.go
@@ -9,6 +9,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
exec "github.com/zitadel/zitadel/internal/repository/execution"
"github.com/zitadel/zitadel/internal/repository/instance"
+ "github.com/zitadel/zitadel/internal/repository/target"
)
const (
@@ -78,6 +79,15 @@ func (p *executionProjection) Reducers() []handler.AggregateReducer {
},
},
},
+ {
+ Aggregate: target.AggregateType,
+ EventReducers: []handler.EventReducer{
+ {
+ Event: target.RemovedEventType,
+ Reduce: p.reduceTargetRemoved,
+ },
+ },
+ },
{
Aggregate: instance.AggregateType,
EventReducers: []handler.EventReducer{
@@ -152,6 +162,21 @@ func (p *executionProjection) reduceExecutionSet(event eventstore.Event) (*handl
return handler.NewMultiStatement(e, stmts...), nil
}
+func (p *executionProjection) reduceTargetRemoved(event eventstore.Event) (*handler.Statement, error) {
+ e, err := assertEvent[*target.RemovedEvent](event)
+ if err != nil {
+ return nil, err
+ }
+ return handler.NewDeleteStatement(
+ e,
+ []handler.Condition{
+ handler.NewCond(ExecutionTargetInstanceIDCol, e.Aggregate().InstanceID),
+ handler.NewCond(ExecutionTargetTargetIDCol, e.Aggregate().ID),
+ },
+ handler.WithTableSuffix(ExecutionTargetSuffix),
+ ), nil
+}
+
func (p *executionProjection) reduceExecutionRemoved(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*exec.RemovedEvent](event)
if err != nil {
diff --git a/internal/query/projection/execution_test.go b/internal/query/projection/execution_test.go
index 27d6e89258..aecae6905a 100644
--- a/internal/query/projection/execution_test.go
+++ b/internal/query/projection/execution_test.go
@@ -7,6 +7,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
exec "github.com/zitadel/zitadel/internal/repository/execution"
"github.com/zitadel/zitadel/internal/repository/instance"
+ "github.com/zitadel/zitadel/internal/repository/target"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -79,6 +80,35 @@ func TestExecutionProjection_reduces(t *testing.T) {
},
},
},
+ {
+ name: "reduceTargetRemoved",
+ args: args{
+ event: getEvent(
+ testEvent(
+ target.RemovedEventType,
+ target.AggregateType,
+ []byte(`{}`),
+ ),
+ eventstore.GenericEventMapper[target.RemovedEvent],
+ ),
+ },
+ reduce: (&executionProjection{}).reduceTargetRemoved,
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("target"),
+ sequence: 15,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "DELETE FROM projections.executions1_targets WHERE (instance_id = $1) AND (target_id = $2)",
+ expectedArgs: []interface{}{
+ "instance-id",
+ "agg-id",
+ },
+ },
+ },
+ },
+ },
+ },
{
name: "reduceExecutionRemoved",
args: args{
From 02acc932425c9b3519b8ad6c1dc334ad134319c5 Mon Sep 17 00:00:00 2001
From: Ramon
Date: Wed, 30 Apr 2025 15:20:39 +0200
Subject: [PATCH 034/181] fix: Improve Actions V2 translations (#9826)
# Which Problems Are Solved
The translation for event was not loaded correctly.

# How the Problems Are Solved
Correct translations to have the correct key.
# Additional Changes
Improved the translation for all events.
---
.../actions-two-add-action-condition.component.html | 2 +-
console/src/assets/i18n/bg.json | 3 ++-
console/src/assets/i18n/cs.json | 3 ++-
console/src/assets/i18n/de.json | 3 ++-
console/src/assets/i18n/en.json | 3 ++-
console/src/assets/i18n/es.json | 3 ++-
console/src/assets/i18n/fr.json | 3 ++-
console/src/assets/i18n/hu.json | 3 ++-
console/src/assets/i18n/id.json | 3 ++-
console/src/assets/i18n/it.json | 3 ++-
console/src/assets/i18n/ja.json | 3 ++-
console/src/assets/i18n/ko.json | 3 ++-
console/src/assets/i18n/mk.json | 3 ++-
console/src/assets/i18n/nl.json | 3 ++-
console/src/assets/i18n/pl.json | 3 ++-
console/src/assets/i18n/pt.json | 3 ++-
console/src/assets/i18n/ro.json | 3 ++-
console/src/assets/i18n/ru.json | 3 ++-
console/src/assets/i18n/sv.json | 3 ++-
console/src/assets/i18n/zh.json | 3 ++-
20 files changed, 39 insertions(+), 20 deletions(-)
diff --git a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-condition/actions-two-add-action-condition.component.html b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-condition/actions-two-add-action-condition.component.html
index 401e5e521d..f0248f45a2 100644
--- a/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-condition/actions-two-add-action-condition.component.html
+++ b/console/src/app/modules/actions-two/actions-two-add-action/actions-two-add-action-condition/actions-two-add-action-condition.component.html
@@ -84,7 +84,7 @@
{{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.ALL.TITLE' | translate }}
{{
- 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.ALL.DESCRIPTION' | translate
+ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.ALL_EVENTS' | translate
}}
diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json
index 30d9c53763..b98204a917 100644
--- a/console/src/assets/i18n/bg.json
+++ b/console/src/assets/i18n/bg.json
@@ -536,7 +536,7 @@
"TYPES": {
"request": "Заявка",
"response": "Отговор",
- "events": "Събития",
+ "event": "Събития",
"function": "Функция"
},
"DIALOG": {
@@ -567,6 +567,7 @@
"TITLE": "Всички",
"DESCRIPTION": "Изберете това, ако искате да изпълните действието си при всяка заявка"
},
+ "ALL_EVENTS": "Изберете това, ако искате действието да се изпълнява при всяко събитие",
"SELECT_SERVICE": {
"TITLE": "Избор на услуга",
"DESCRIPTION": "Изберете услуга на Zitadel за вашето действие."
diff --git a/console/src/assets/i18n/cs.json b/console/src/assets/i18n/cs.json
index ee43e86822..390c5dcdbd 100644
--- a/console/src/assets/i18n/cs.json
+++ b/console/src/assets/i18n/cs.json
@@ -537,7 +537,7 @@
"TYPES": {
"request": "Požadavek",
"response": "Odpověď",
- "events": "Události",
+ "event": "Události",
"function": "Funkce"
},
"DIALOG": {
@@ -568,6 +568,7 @@
"TITLE": "Všechny",
"DESCRIPTION": "Vyberte tuto možnost, pokud chcete spustit akci pro každý požadavek"
},
+ "ALL_EVENTS": "Vyberte toto, pokud chcete spustit akci při každé události",
"SELECT_SERVICE": {
"TITLE": "Vybrat službu",
"DESCRIPTION": "Vyberte službu Zitadel pro svou akci."
diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json
index 3993674992..e73c883bd2 100644
--- a/console/src/assets/i18n/de.json
+++ b/console/src/assets/i18n/de.json
@@ -537,7 +537,7 @@
"TYPES": {
"request": "Anfrage",
"response": "Antwort",
- "events": "Ereignisse",
+ "event": "Ereignisse",
"function": "Funktion"
},
"DIALOG": {
@@ -568,6 +568,7 @@
"TITLE": "Alle",
"DESCRIPTION": "Wählen Sie dies aus, wenn Sie Ihre Aktion bei jeder Anfrage ausführen möchten"
},
+ "ALL_EVENTS": "Wähle dies aus, wenn du deine Aktion bei jedem Ereignis ausführen möchtest",
"SELECT_SERVICE": {
"TITLE": "Dienst auswählen",
"DESCRIPTION": "Wählen Sie einen Zitadel-Dienst für Ihre Aktion aus."
diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json
index fd81bfd353..5e2cc3f4c9 100644
--- a/console/src/assets/i18n/en.json
+++ b/console/src/assets/i18n/en.json
@@ -537,7 +537,7 @@
"TYPES": {
"request": "Request",
"response": "Response",
- "events": "Events",
+ "event": "Events",
"function": "Function"
},
"DIALOG": {
@@ -568,6 +568,7 @@
"TITLE": "All",
"DESCRIPTION": "Select this if you want to run your action on every request"
},
+ "ALL_EVENTS": "Select this if you want to run your action on every event",
"SELECT_SERVICE": {
"TITLE": "Select Service",
"DESCRIPTION": "Choose a Zitadel Service for you action."
diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json
index aec024eacb..198bb3ca8b 100644
--- a/console/src/assets/i18n/es.json
+++ b/console/src/assets/i18n/es.json
@@ -537,7 +537,7 @@
"TYPES": {
"request": "Solicitud",
"response": "Respuesta",
- "events": "Eventos",
+ "event": "Eventos",
"function": "Función"
},
"DIALOG": {
@@ -568,6 +568,7 @@
"TITLE": "Todas",
"DESCRIPTION": "Selecciona esto si quieres ejecutar tu acción en cada solicitud"
},
+ "ALL_EVENTS": "Selecciona esto si quieres ejecutar tu acción en cada evento",
"SELECT_SERVICE": {
"TITLE": "Seleccionar servicio",
"DESCRIPTION": "Elige un servicio de Zitadel para tu acción."
diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json
index 05e34ad846..0d66c4193e 100644
--- a/console/src/assets/i18n/fr.json
+++ b/console/src/assets/i18n/fr.json
@@ -537,7 +537,7 @@
"TYPES": {
"request": "Requête",
"response": "Réponse",
- "events": "Événements",
+ "event": "Événements",
"function": "Fonction"
},
"DIALOG": {
@@ -568,6 +568,7 @@
"TITLE": "Tous",
"DESCRIPTION": "Sélectionnez ceci si vous souhaitez exécuter votre action sur chaque requête"
},
+ "ALL_EVENTS": "Sélectionnez ceci si vous souhaitez exécuter votre action à chaque événement",
"SELECT_SERVICE": {
"TITLE": "Sélectionner un service",
"DESCRIPTION": "Choisissez un service Zitadel pour votre action."
diff --git a/console/src/assets/i18n/hu.json b/console/src/assets/i18n/hu.json
index d46bc96153..96d1fe16df 100644
--- a/console/src/assets/i18n/hu.json
+++ b/console/src/assets/i18n/hu.json
@@ -537,7 +537,7 @@
"TYPES": {
"request": "Kérés",
"response": "Válasz",
- "events": "Események",
+ "event": "Események",
"function": "Függvény"
},
"DIALOG": {
@@ -568,6 +568,7 @@
"TITLE": "Összes",
"DESCRIPTION": "Válassza ezt, ha minden kérésnél futtatni szeretné a műveletet"
},
+ "ALL_EVENTS": "Válaszd ezt, ha minden eseménynél futtatni szeretnéd a műveletet",
"SELECT_SERVICE": {
"TITLE": "Szolgáltatás kiválasztása",
"DESCRIPTION": "Válasszon egy Zitadel szolgáltatást a művelethez."
diff --git a/console/src/assets/i18n/id.json b/console/src/assets/i18n/id.json
index f8831a224d..ca788a9467 100644
--- a/console/src/assets/i18n/id.json
+++ b/console/src/assets/i18n/id.json
@@ -504,7 +504,7 @@
"TYPES": {
"request": "Permintaan",
"response": "Respons",
- "events": "Peristiwa",
+ "event": "Peristiwa",
"function": "Fungsi"
},
"DIALOG": {
@@ -535,6 +535,7 @@
"TITLE": "Semua",
"DESCRIPTION": "Pilih ini jika Anda ingin menjalankan tindakan Anda pada setiap permintaan"
},
+ "ALL_EVENTS": "Pilih ini jika Anda ingin menjalankan aksi Anda pada setiap peristiwa",
"SELECT_SERVICE": {
"TITLE": "Pilih Layanan",
"DESCRIPTION": "Pilih Layanan Zitadel untuk tindakan Anda."
diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json
index 5dad683bca..60266bdac5 100644
--- a/console/src/assets/i18n/it.json
+++ b/console/src/assets/i18n/it.json
@@ -536,7 +536,7 @@
"TYPES": {
"request": "Richiesta",
"response": "Risposta",
- "events": "Eventi",
+ "event": "Eventi",
"function": "Funzione"
},
"DIALOG": {
@@ -567,6 +567,7 @@
"TITLE": "Tutte",
"DESCRIPTION": "Seleziona questa opzione se vuoi eseguire la tua azione su ogni richiesta"
},
+ "ALL_EVENTS": "Seleziona questo se vuoi eseguire la tua azione a ogni evento",
"SELECT_SERVICE": {
"TITLE": "Seleziona servizio",
"DESCRIPTION": "Scegli un servizio Zitadel per la tua azione."
diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json
index f09dfeb564..288d491ce7 100644
--- a/console/src/assets/i18n/ja.json
+++ b/console/src/assets/i18n/ja.json
@@ -537,7 +537,7 @@
"TYPES": {
"request": "リクエスト",
"response": "レスポンス",
- "events": "イベント",
+ "event": "イベント",
"function": "関数"
},
"DIALOG": {
@@ -568,6 +568,7 @@
"TITLE": "すべて",
"DESCRIPTION": "すべてのリクエストでアクションを実行する場合は、これを選択します"
},
+ "ALL_EVENTS": "すべてのイベントでアクションを実行する場合はこれを選択してください",
"SELECT_SERVICE": {
"TITLE": "サービスを選択",
"DESCRIPTION": "アクションのZitadelサービスを選択します。"
diff --git a/console/src/assets/i18n/ko.json b/console/src/assets/i18n/ko.json
index 304f52e127..437c43a3a1 100644
--- a/console/src/assets/i18n/ko.json
+++ b/console/src/assets/i18n/ko.json
@@ -537,7 +537,7 @@
"TYPES": {
"request": "요청",
"response": "응답",
- "events": "이벤트",
+ "event": "이벤트",
"function": "함수"
},
"DIALOG": {
@@ -568,6 +568,7 @@
"TITLE": "모두",
"DESCRIPTION": "모든 요청에서 작업을 실행하려면 이것을 선택하십시오."
},
+ "ALL_EVENTS": "모든 이벤트에서 작업을 실행하려면 이 항목을 선택하세요",
"SELECT_SERVICE": {
"TITLE": "서비스 선택",
"DESCRIPTION": "작업에 대한 Zitadel 서비스를 선택하십시오."
diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json
index 1ab4dce534..2e62723939 100644
--- a/console/src/assets/i18n/mk.json
+++ b/console/src/assets/i18n/mk.json
@@ -537,7 +537,7 @@
"TYPES": {
"request": "Барање",
"response": "Одговор",
- "events": "Настани",
+ "event": "Настани",
"function": "Функција"
},
"DIALOG": {
@@ -568,6 +568,7 @@
"TITLE": "Сите",
"DESCRIPTION": "Изберете го ова ако сакате да ја извршите вашата акција на секое барање"
},
+ "ALL_EVENTS": "Изберете го ова ако сакате вашата акција да се извршува на секој настан",
"SELECT_SERVICE": {
"TITLE": "Изберете услуга",
"DESCRIPTION": "Изберете Zitadel услуга за вашата акција."
diff --git a/console/src/assets/i18n/nl.json b/console/src/assets/i18n/nl.json
index a08f62ed20..7e549f64ba 100644
--- a/console/src/assets/i18n/nl.json
+++ b/console/src/assets/i18n/nl.json
@@ -537,7 +537,7 @@
"TYPES": {
"request": "Verzoek",
"response": "Reactie",
- "events": "Gebeurtenissen",
+ "event": "Gebeurtenissen",
"function": "Functie"
},
"DIALOG": {
@@ -568,6 +568,7 @@
"TITLE": "Alle",
"DESCRIPTION": "Selecteer dit als u uw actie bij elk verzoek wilt uitvoeren"
},
+ "ALL_EVENTS": "Selecteer dit als je je actie bij elk evenement wilt uitvoeren",
"SELECT_SERVICE": {
"TITLE": "Service selecteren",
"DESCRIPTION": "Kies een Zitadel-service voor uw actie."
diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json
index 3902378af6..2f18c343f7 100644
--- a/console/src/assets/i18n/pl.json
+++ b/console/src/assets/i18n/pl.json
@@ -536,7 +536,7 @@
"TYPES": {
"request": "Żądanie",
"response": "Odpowiedź",
- "events": "Zdarzenia",
+ "event": "Zdarzenia",
"function": "Funkcja"
},
"DIALOG": {
@@ -567,6 +567,7 @@
"TITLE": "Wszystkie",
"DESCRIPTION": "Wybierz tę opcję, jeśli chcesz uruchomić akcję dla każdego żądania"
},
+ "ALL_EVENTS": "Wybierz to, jeśli chcesz uruchamiać swoją akcję przy każdym zdarzeniu",
"SELECT_SERVICE": {
"TITLE": "Wybierz usługę",
"DESCRIPTION": "Wybierz usługę Zitadel dla swojej akcji."
diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json
index 016785f2c8..08181f6ead 100644
--- a/console/src/assets/i18n/pt.json
+++ b/console/src/assets/i18n/pt.json
@@ -537,7 +537,7 @@
"TYPES": {
"request": "Solicitação",
"response": "Resposta",
- "events": "Eventos",
+ "event": "Eventos",
"function": "Função"
},
"DIALOG": {
@@ -568,6 +568,7 @@
"TITLE": "Todas",
"DESCRIPTION": "Selecione isso se você quiser executar sua ação em cada solicitação"
},
+ "ALL_EVENTS": "Selecione isto se quiser executar sua ação em cada evento",
"SELECT_SERVICE": {
"TITLE": "Selecionar Serviço",
"DESCRIPTION": "Escolha um Serviço Zitadel para sua ação."
diff --git a/console/src/assets/i18n/ro.json b/console/src/assets/i18n/ro.json
index 4ad511c466..b07897f316 100644
--- a/console/src/assets/i18n/ro.json
+++ b/console/src/assets/i18n/ro.json
@@ -537,7 +537,7 @@
"TYPES": {
"request": "Cerere",
"response": "Răspuns",
- "events": "Evenimente",
+ "event": "Evenimente",
"function": "Funcție"
},
"DIALOG": {
@@ -568,6 +568,7 @@
"TITLE": "Toate",
"DESCRIPTION": "Selectați aceasta dacă doriți să rulați acțiunea la fiecare cerere"
},
+ "ALL_EVENTS": "Selectează aceasta dacă vrei să rulezi acțiunea ta la fiecare eveniment",
"SELECT_SERVICE": {
"TITLE": "Selectați Serviciul",
"DESCRIPTION": "Alegeți un Serviciu Zitadel pentru acțiunea dvs."
diff --git a/console/src/assets/i18n/ru.json b/console/src/assets/i18n/ru.json
index 43bc266be0..c6ef31499e 100644
--- a/console/src/assets/i18n/ru.json
+++ b/console/src/assets/i18n/ru.json
@@ -537,7 +537,7 @@
"TYPES": {
"request": "Запрос",
"response": "Ответ",
- "events": "События",
+ "event": "События",
"function": "Функция"
},
"DIALOG": {
@@ -568,6 +568,7 @@
"TITLE": "Все",
"DESCRIPTION": "Выберите это, если вы хотите запустить свое действие при каждом запросе"
},
+ "ALL_EVENTS": "Выберите это, если хотите выполнять действие при каждом событии",
"SELECT_SERVICE": {
"TITLE": "Выбрать службу",
"DESCRIPTION": "Выберите службу Zitadel для вашего действия."
diff --git a/console/src/assets/i18n/sv.json b/console/src/assets/i18n/sv.json
index 00b7854603..c356e635e0 100644
--- a/console/src/assets/i18n/sv.json
+++ b/console/src/assets/i18n/sv.json
@@ -537,7 +537,7 @@
"TYPES": {
"request": "Förfrågan",
"response": "Svar",
- "events": "Händelser",
+ "event": "Händelser",
"function": "Funktion"
},
"DIALOG": {
@@ -568,6 +568,7 @@
"TITLE": "Alla",
"DESCRIPTION": "Välj detta om du vill köra din åtgärd på varje förfrågan"
},
+ "ALL_EVENTS": "Välj detta om du vill köra din åtgärd vid varje händelse",
"SELECT_SERVICE": {
"TITLE": "Välj tjänst",
"DESCRIPTION": "Välj en Zitadel-tjänst för din åtgärd."
diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json
index 496b3d528e..8be3316b0b 100644
--- a/console/src/assets/i18n/zh.json
+++ b/console/src/assets/i18n/zh.json
@@ -537,7 +537,7 @@
"TYPES": {
"request": "请求",
"response": "响应",
- "events": "事件",
+ "event": "事件",
"function": "函数"
},
"DIALOG": {
@@ -568,6 +568,7 @@
"TITLE": "全部",
"DESCRIPTION": "如果您希望在每个请求上运行您的操作,请选择此项"
},
+ "ALL_EVENTS": "如果您想在每个事件上运行操作,请选择此项",
"SELECT_SERVICE": {
"TITLE": "选择服务",
"DESCRIPTION": "为您的操作选择一个 Zitadel 服务。"
From 74ace1aec31eb6085c1b871c7465524f5f9d13cf Mon Sep 17 00:00:00 2001
From: Elio Bischof
Date: Thu, 1 May 2025 07:41:57 +0200
Subject: [PATCH 035/181] fix(actions): default sorting column to creation date
(#9795)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Which Problems Are Solved
The sorting column of action targets and executions defaults to the ID
column instead of the creation date column.
This is only relevant, if the sorting column is explicitly passed as
unspecified.
If the sorting column is not passed, it correctly defaults to the
creation date.
```bash
# ❌ Sorts by ID
grpcurl -plaintext -H "Authorization: Bearer ${ZITADEL_ACCESS_TOKEN}" -d '{"sortingColumn": "TARGET_FIELD_NAME_UNSPECIFIED"}' localhost:8080 zitadel.action.v2beta.ActionService.ListTargets
# ❌ Sorts by ID
grpcurl -plaintext -H "Authorization: Bearer ${ZITADEL_ACCESS_TOKEN}" -d '{"sortingColumn": 0}' localhost:8080 zitadel.action.v2beta.ActionService.ListTargets
# ✅ Sorts by creation date
grpcurl -plaintext -H "Authorization: Bearer ${ZITADEL_ACCESS_TOKEN}" localhost:8080 zitadel.action.v2beta.ActionService.ListTargets
```
# How the Problems Are Solved
`action.TargetFieldName_TARGET_FIELD_NAME_UNSPECIFIED` maps to the
sorting column `query.TargetColumnCreationDate`.
# Additional Context
As IDs are also generated in ascending, like creation dates, the the bug
probably only causes unexpected behavior for cases, where the ID is
specified during target or execution creation. This is currently not
supported, so this bug probably has no impact at all. It doesn't need to
be backported.
Found during implementation of #9763
Co-authored-by: Livio Spring
---
internal/api/grpc/action/v2beta/query.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/internal/api/grpc/action/v2beta/query.go b/internal/api/grpc/action/v2beta/query.go
index 66bafa4e7d..1dbe80a8f7 100644
--- a/internal/api/grpc/action/v2beta/query.go
+++ b/internal/api/grpc/action/v2beta/query.go
@@ -164,7 +164,7 @@ func targetFieldNameToSortingColumn(field *action.TargetFieldName) query.Column
}
switch *field {
case action.TargetFieldName_TARGET_FIELD_NAME_UNSPECIFIED:
- return query.TargetColumnID
+ return query.TargetColumnCreationDate
case action.TargetFieldName_TARGET_FIELD_NAME_ID:
return query.TargetColumnID
case action.TargetFieldName_TARGET_FIELD_NAME_CREATED_DATE:
@@ -193,7 +193,7 @@ func executionFieldNameToSortingColumn(field *action.ExecutionFieldName) query.C
}
switch *field {
case action.ExecutionFieldName_EXECUTION_FIELD_NAME_UNSPECIFIED:
- return query.ExecutionColumnID
+ return query.ExecutionColumnCreationDate
case action.ExecutionFieldName_EXECUTION_FIELD_NAME_ID:
return query.ExecutionColumnID
case action.ExecutionFieldName_EXECUTION_FIELD_NAME_CREATED_DATE:
From bb56b362a755b5e07dfd364db59e1c02cd21690e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tim=20M=C3=B6hlmann?=
Date: Fri, 2 May 2025 13:40:22 +0200
Subject: [PATCH 036/181] perf(eventstore): add instance position index (#9837)
# Which Problems Are Solved
Some projection queries took a long time to run. It seems that 1 or more
queries couldn't make proper use of the `es_projection` index. This
might be because of a specific complexity aggregate_type and event_type
arguments, making the index unfeasible for postgres.
# How the Problems Are Solved
Following the index recommendation, add and index that covers just
instance_id and position.
# Additional Changes
- none
# Additional Context
- Related to https://github.com/zitadel/zitadel/issues/9832
---
cmd/setup/54.go | 27 +++++++++++++++++++++++++++
cmd/setup/54.sql | 1 +
2 files changed, 28 insertions(+)
create mode 100644 cmd/setup/54.go
create mode 100644 cmd/setup/54.sql
diff --git a/cmd/setup/54.go b/cmd/setup/54.go
new file mode 100644
index 0000000000..3dd2f60abe
--- /dev/null
+++ b/cmd/setup/54.go
@@ -0,0 +1,27 @@
+package setup
+
+import (
+ "context"
+ _ "embed"
+
+ "github.com/zitadel/zitadel/internal/database"
+ "github.com/zitadel/zitadel/internal/eventstore"
+)
+
+var (
+ //go:embed 54.sql
+ instancePositionIndex string
+)
+
+type InstancePositionIndex struct {
+ dbClient *database.DB
+}
+
+func (mig *InstancePositionIndex) Execute(ctx context.Context, _ eventstore.Event) error {
+ _, err := mig.dbClient.ExecContext(ctx, instancePositionIndex)
+ return err
+}
+
+func (mig *InstancePositionIndex) String() string {
+ return "54_instance_position_index"
+}
diff --git a/cmd/setup/54.sql b/cmd/setup/54.sql
new file mode 100644
index 0000000000..1dca8c7575
--- /dev/null
+++ b/cmd/setup/54.sql
@@ -0,0 +1 @@
+CREATE INDEX CONCURRENTLY IF NOT EXISTS es_instance_position ON eventstore.events2 (instance_id, position);
From b1e60e7398d677f08b06fd7715227f70b7ca1162 Mon Sep 17 00:00:00 2001
From: Livio Spring
Date: Fri, 2 May 2025 13:44:24 +0200
Subject: [PATCH 037/181] Merge commit from fork
* fix: prevent intent token reuse and add expiry
* fix duplicate
* fix expiration
---
cmd/defaults.yaml | 3 +
.../v2/integration_test/session_test.go | 80 +++++++++++-
.../v2beta/integration_test/session_test.go | 80 +++++++++++-
.../user/v2/integration_test/user_test.go | 52 ++++++--
internal/api/grpc/user/v2/intent.go | 17 ++-
.../user/v2beta/integration_test/user_test.go | 52 ++++++--
internal/api/grpc/user/v2beta/user.go | 12 +-
internal/api/idp/idp.go | 2 +-
internal/api/idp/idp_test.go | 38 +++---
internal/command/command.go | 2 +
internal/command/idp_intent.go | 20 ++-
internal/command/idp_intent_model.go | 29 ++++-
internal/command/idp_intent_test.go | 56 ++++++---
internal/command/session.go | 51 ++++----
internal/command/session_test.go | 114 +++++++++++++++++-
.../config/systemdefaults/system_defaults.go | 19 +--
internal/domain/idp.go | 1 +
internal/idp/providers/apple/session.go | 2 +
internal/idp/providers/azuread/session.go | 10 ++
internal/idp/providers/jwt/session.go | 7 ++
internal/idp/providers/ldap/session.go | 4 +
internal/idp/providers/oauth/session.go | 8 ++
internal/idp/providers/oidc/session.go | 8 ++
internal/idp/providers/saml/session.go | 8 ++
internal/idp/session.go | 2 +
internal/integration/client.go | 17 +++
internal/integration/sink/server.go | 42 +++++--
internal/repository/idpintent/eventstore.go | 1 +
internal/repository/idpintent/intent.go | 40 ++++++
internal/static/i18n/bg.yaml | 1 +
internal/static/i18n/cs.yaml | 1 +
internal/static/i18n/de.yaml | 1 +
internal/static/i18n/en.yaml | 1 +
internal/static/i18n/es.yaml | 1 +
internal/static/i18n/fr.yaml | 1 +
internal/static/i18n/hu.yaml | 1 +
internal/static/i18n/id.yaml | 1 +
internal/static/i18n/it.yaml | 1 +
internal/static/i18n/ja.yaml | 1 +
internal/static/i18n/ko.yaml | 1 +
internal/static/i18n/mk.yaml | 1 +
internal/static/i18n/nl.yaml | 1 +
internal/static/i18n/pl.yaml | 1 +
internal/static/i18n/pt.yaml | 1 +
internal/static/i18n/ro.yaml | 1 +
internal/static/i18n/ru.yaml | 1 +
internal/static/i18n/sv.yaml | 1 +
internal/static/i18n/zh.yaml | 1 +
48 files changed, 673 insertions(+), 123 deletions(-)
diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml
index 6ab01ab35b..0d71b4d817 100644
--- a/cmd/defaults.yaml
+++ b/cmd/defaults.yaml
@@ -735,6 +735,9 @@ SystemDefaults:
DefaultQueryLimit: 100 # ZITADEL_SYSTEMDEFAULTS_DEFAULTQUERYLIMIT
# MaxQueryLimit limits the number of items that can be queried in a single v3 API search request with explicitly passing a limit.
MaxQueryLimit: 1000 # ZITADEL_SYSTEMDEFAULTS_MAXQUERYLIMIT
+ # The maximum duration of the IDP intent lifetime after which the IDP intent expires and can not be retrieved or used anymore.
+ # Note that this time is measured only after the IdP intent was successful and not after the IDP intent was created.
+ MaxIdPIntentLifetime: 1h # ZITADEL_SYSTEMDEFAULTS_MAXIDPINTENTLIFETIME
Actions:
HTTP:
diff --git a/internal/api/grpc/session/v2/integration_test/session_test.go b/internal/api/grpc/session/v2/integration_test/session_test.go
index b9a060c749..0982a56121 100644
--- a/internal/api/grpc/session/v2/integration_test/session_test.go
+++ b/internal/api/grpc/session/v2/integration_test/session_test.go
@@ -354,7 +354,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) {
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
- intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId())
+ intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour))
require.NoError(t, err)
updateResp, err := Client.SetSession(LoginCTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
@@ -372,7 +372,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) {
func TestServer_CreateSession_successfulIntent_instant(t *testing.T) {
idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId()
- intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId())
+ intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour))
require.NoError(t, err)
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
Checks: &session.Checks{
@@ -396,7 +396,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) {
// successful intent without known / linked user
idpUserID := "id"
- intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, idpUserID, "")
+ intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, idpUserID, "", time.Now().Add(time.Hour))
// link the user (with info from intent)
Instance.CreateUserIDPlink(CTX, User.GetUserId(), idpUserID, idpID, User.GetUserId())
@@ -447,6 +447,80 @@ func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) {
require.Error(t, err)
}
+func TestServer_CreateSession_reuseIntent(t *testing.T) {
+ idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId()
+ createResp, err := Client.CreateSession(LoginCTX, &session.CreateSessionRequest{
+ Checks: &session.Checks{
+ User: &session.CheckUser{
+ Search: &session.CheckUser_UserId{
+ UserId: User.GetUserId(),
+ },
+ },
+ },
+ })
+ require.NoError(t, err)
+ verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
+
+ intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour))
+ require.NoError(t, err)
+ updateResp, err := Client.SetSession(LoginCTX, &session.SetSessionRequest{
+ SessionId: createResp.GetSessionId(),
+ Checks: &session.Checks{
+ IdpIntent: &session.CheckIDPIntent{
+ IdpIntentId: intentID,
+ IdpIntentToken: token,
+ },
+ },
+ })
+ require.NoError(t, err)
+ verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantIntentFactor)
+
+ // the reuse of the intent token is not allowed, not even on the same session
+ session2, err := Client.SetSession(LoginCTX, &session.SetSessionRequest{
+ SessionId: createResp.GetSessionId(),
+ Checks: &session.Checks{
+ IdpIntent: &session.CheckIDPIntent{
+ IdpIntentId: intentID,
+ IdpIntentToken: token,
+ },
+ },
+ })
+ require.Error(t, err)
+ _ = session2
+}
+
+func TestServer_CreateSession_expiredIntent(t *testing.T) {
+ idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId()
+ createResp, err := Client.CreateSession(LoginCTX, &session.CreateSessionRequest{
+ Checks: &session.Checks{
+ User: &session.CheckUser{
+ Search: &session.CheckUser_UserId{
+ UserId: User.GetUserId(),
+ },
+ },
+ },
+ })
+ require.NoError(t, err)
+ verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
+
+ intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Second))
+ require.NoError(t, err)
+
+ // wait for the intent to expire
+ time.Sleep(2 * time.Second)
+
+ _, err = Client.SetSession(LoginCTX, &session.SetSessionRequest{
+ SessionId: createResp.GetSessionId(),
+ Checks: &session.Checks{
+ IdpIntent: &session.CheckIDPIntent{
+ IdpIntentId: intentID,
+ IdpIntentToken: token,
+ },
+ },
+ })
+ require.Error(t, err)
+}
+
func registerTOTP(ctx context.Context, t *testing.T, userID string) (secret string) {
resp, err := Instance.Client.UserV2.RegisterTOTP(ctx, &user.RegisterTOTPRequest{
UserId: userID,
diff --git a/internal/api/grpc/session/v2beta/integration_test/session_test.go b/internal/api/grpc/session/v2beta/integration_test/session_test.go
index d0fc1179ef..4c189e0f80 100644
--- a/internal/api/grpc/session/v2beta/integration_test/session_test.go
+++ b/internal/api/grpc/session/v2beta/integration_test/session_test.go
@@ -354,7 +354,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) {
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
- intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId())
+ intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour))
require.NoError(t, err)
updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
@@ -372,7 +372,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) {
func TestServer_CreateSession_successfulIntent_instant(t *testing.T) {
idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId()
- intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId())
+ intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour))
require.NoError(t, err)
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
Checks: &session.Checks{
@@ -396,7 +396,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) {
// successful intent without known / linked user
idpUserID := "id"
- intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId())
+ intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour))
require.NoError(t, err)
// link the user (with info from intent)
@@ -448,6 +448,80 @@ func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) {
require.Error(t, err)
}
+func TestServer_CreateSession_reuseIntent(t *testing.T) {
+ idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId()
+ createResp, err := Client.CreateSession(IAMOwnerCTX, &session.CreateSessionRequest{
+ Checks: &session.Checks{
+ User: &session.CheckUser{
+ Search: &session.CheckUser_UserId{
+ UserId: User.GetUserId(),
+ },
+ },
+ },
+ })
+ require.NoError(t, err)
+ verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
+
+ intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour))
+ require.NoError(t, err)
+ updateResp, err := Client.SetSession(IAMOwnerCTX, &session.SetSessionRequest{
+ SessionId: createResp.GetSessionId(),
+ Checks: &session.Checks{
+ IdpIntent: &session.CheckIDPIntent{
+ IdpIntentId: intentID,
+ IdpIntentToken: token,
+ },
+ },
+ })
+ require.NoError(t, err)
+ verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantIntentFactor)
+
+ // the reuse of the intent token is not allowed, not even on the same session
+ session2, err := Client.SetSession(IAMOwnerCTX, &session.SetSessionRequest{
+ SessionId: createResp.GetSessionId(),
+ Checks: &session.Checks{
+ IdpIntent: &session.CheckIDPIntent{
+ IdpIntentId: intentID,
+ IdpIntentToken: token,
+ },
+ },
+ })
+ require.Error(t, err)
+ _ = session2
+}
+
+func TestServer_CreateSession_expiredIntent(t *testing.T) {
+ idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId()
+ createResp, err := Client.CreateSession(IAMOwnerCTX, &session.CreateSessionRequest{
+ Checks: &session.Checks{
+ User: &session.CheckUser{
+ Search: &session.CheckUser_UserId{
+ UserId: User.GetUserId(),
+ },
+ },
+ },
+ })
+ require.NoError(t, err)
+ verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
+
+ intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Second))
+ require.NoError(t, err)
+
+ // wait for the intent to expire
+ time.Sleep(2 * time.Second)
+
+ _, err = Client.SetSession(IAMOwnerCTX, &session.SetSessionRequest{
+ SessionId: createResp.GetSessionId(),
+ Checks: &session.Checks{
+ IdpIntent: &session.CheckIDPIntent{
+ IdpIntentId: intentID,
+ IdpIntentToken: token,
+ },
+ },
+ })
+ require.Error(t, err)
+}
+
func registerTOTP(ctx context.Context, t *testing.T, userID string) (secret string) {
resp, err := Instance.Client.UserV2.RegisterTOTP(ctx, &user.RegisterTOTPRequest{
UserId: userID,
diff --git a/internal/api/grpc/user/v2/integration_test/user_test.go b/internal/api/grpc/user/v2/integration_test/user_test.go
index bf396fd25d..70e670bacc 100644
--- a/internal/api/grpc/user/v2/integration_test/user_test.go
+++ b/internal/api/grpc/user/v2/integration_test/user_test.go
@@ -2121,22 +2121,36 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
authURL, err := url.Parse(Instance.CreateIntent(CTX, oauthIdpID).GetAuthUrl())
require.NoError(t, err)
intentID := authURL.Query().Get("state")
+ expiry := time.Now().Add(1 * time.Hour)
+ expiryFormatted := expiry.Round(time.Millisecond).UTC().Format("2006-01-02T15:04:05.999Z07:00")
- successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "")
+ intentUser := Instance.CreateHumanUser(IamCTX)
+ _, err = Instance.CreateUserIDPlink(IamCTX, intentUser.GetUserId(), "idpUserID", oauthIdpID, "username")
require.NoError(t, err)
- successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user")
+
+ successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "", expiry)
require.NoError(t, err)
- oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "")
+ successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", expiry)
require.NoError(t, err)
- oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user")
+ successfulExpiredID, expiredToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", time.Now().Add(time.Second))
+ require.NoError(t, err)
+ // make sure the intent is expired
+ time.Sleep(2 * time.Second)
+ successfulConsumedID, consumedToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "idpUserID", intentUser.GetUserId(), expiry)
+ require.NoError(t, err)
+ // make sure the intent is consumed
+ Instance.CreateIntentSession(t, IamCTX, intentUser.GetUserId(), successfulConsumedID, consumedToken)
+ oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "", expiry)
+ require.NoError(t, err)
+ oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user", expiry)
require.NoError(t, err)
ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "")
require.NoError(t, err)
ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "user")
require.NoError(t, err)
- samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "")
+ samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "", expiry)
require.NoError(t, err)
- samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user")
+ samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user", expiry)
require.NoError(t, err)
type args struct {
ctx context.Context
@@ -2260,6 +2274,28 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
},
wantErr: false,
},
+ {
+ name: "retrieve successful expired intent",
+ args: args{
+ CTX,
+ &user.RetrieveIdentityProviderIntentRequest{
+ IdpIntentId: successfulExpiredID,
+ IdpIntentToken: expiredToken,
+ },
+ },
+ wantErr: true,
+ },
+ {
+ name: "retrieve successful consumed intent",
+ args: args{
+ CTX,
+ &user.RetrieveIdentityProviderIntentRequest{
+ IdpIntentId: successfulConsumedID,
+ IdpIntentToken: consumedToken,
+ },
+ },
+ wantErr: true,
+ },
{
name: "retrieve successful oidc intent",
args: args{
@@ -2469,7 +2505,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Saml{
Saml: &user.IDPSAMLAccessInformation{
- Assertion: []byte(" "),
+ Assertion: []byte(fmt.Sprintf(` `, expiryFormatted)),
},
},
IdpId: samlIdpID,
@@ -2518,7 +2554,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Saml{
Saml: &user.IDPSAMLAccessInformation{
- Assertion: []byte(" "),
+ Assertion: []byte(fmt.Sprintf(` `, expiryFormatted)),
},
},
IdpId: samlIdpID,
diff --git a/internal/api/grpc/user/v2/intent.go b/internal/api/grpc/user/v2/intent.go
index 06966edb35..8043a9bdae 100644
--- a/internal/api/grpc/user/v2/intent.go
+++ b/internal/api/grpc/user/v2/intent.go
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
+ "time"
oidc_pkg "github.com/zitadel/oidc/v3/pkg/oidc"
"google.golang.org/protobuf/types/known/structpb"
@@ -71,14 +72,14 @@ func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredenti
if err != nil {
return nil, err
}
- externalUser, userID, attributes, err := s.ldapLogin(ctx, intentWriteModel.IDPID, ldapCredentials.GetUsername(), ldapCredentials.GetPassword())
+ externalUser, userID, session, err := s.ldapLogin(ctx, intentWriteModel.IDPID, ldapCredentials.GetUsername(), ldapCredentials.GetPassword())
if err != nil {
if err := s.command.FailIDPIntent(ctx, intentWriteModel, err.Error()); err != nil {
return nil, err
}
return nil, err
}
- token, err := s.command.SucceedLDAPIDPIntent(ctx, intentWriteModel, externalUser, userID, attributes)
+ token, err := s.command.SucceedLDAPIDPIntent(ctx, intentWriteModel, externalUser, userID, session)
if err != nil {
return nil, err
}
@@ -116,7 +117,7 @@ func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUse
return "", nil
}
-func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, map[string][]string, error) {
+func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, *ldap.Session, error) {
provider, err := s.command.GetProvider(ctx, idpID, "", "")
if err != nil {
return nil, "", nil, err
@@ -137,12 +138,7 @@ func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string
if err != nil {
return nil, "", nil, err
}
-
- attributes := make(map[string][]string, 0)
- for _, item := range session.Entry.Attributes {
- attributes[item.Name] = item.Values
- }
- return externalUser, userID, attributes, nil
+ return externalUser, userID, session, nil
}
func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.RetrieveIdentityProviderIntentRequest) (_ *user.RetrieveIdentityProviderIntentResponse, err error) {
@@ -156,6 +152,9 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.R
if intent.State != domain.IDPIntentStateSucceeded {
return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-nme4gszsvx", "Errors.Intent.NotSucceeded")
}
+ if time.Now().After(intent.ExpiresAt()) {
+ return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-SAf42", "Errors.Intent.Expired")
+ }
idpIntent, err := idpIntentToIDPIntentPb(intent, s.idpAlg)
if err != nil {
return nil, err
diff --git a/internal/api/grpc/user/v2beta/integration_test/user_test.go b/internal/api/grpc/user/v2beta/integration_test/user_test.go
index a81de58761..a5a1309d1a 100644
--- a/internal/api/grpc/user/v2beta/integration_test/user_test.go
+++ b/internal/api/grpc/user/v2beta/integration_test/user_test.go
@@ -2153,22 +2153,36 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
authURL, err := url.Parse(Instance.CreateIntent(CTX, oauthIdpID).GetAuthUrl())
require.NoError(t, err)
intentID := authURL.Query().Get("state")
+ expiry := time.Now().Add(1 * time.Hour)
+ expiryFormatted := expiry.Round(time.Millisecond).UTC().Format("2006-01-02T15:04:05.999Z07:00")
- successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "")
+ intentUser := Instance.CreateHumanUser(IamCTX)
+ _, err = Instance.CreateUserIDPlink(IamCTX, intentUser.GetUserId(), "idpUserID", oauthIdpID, "username")
require.NoError(t, err)
- successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user")
+
+ successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "", expiry)
require.NoError(t, err)
- oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "")
+ successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", expiry)
require.NoError(t, err)
- oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user")
+ successfulExpiredID, expiredToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", time.Now().Add(time.Second))
+ require.NoError(t, err)
+ // make sure the intent is expired
+ time.Sleep(2 * time.Second)
+ successfulConsumedID, consumedToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "idpUserID", intentUser.GetUserId(), expiry)
+ require.NoError(t, err)
+ // make sure the intent is consumed
+ Instance.CreateIntentSession(t, IamCTX, intentUser.GetUserId(), successfulConsumedID, consumedToken)
+ oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "", expiry)
+ require.NoError(t, err)
+ oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user", expiry)
require.NoError(t, err)
ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "")
require.NoError(t, err)
ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "user")
require.NoError(t, err)
- samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "")
+ samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "", expiry)
require.NoError(t, err)
- samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user")
+ samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user", expiry)
require.NoError(t, err)
type args struct {
ctx context.Context
@@ -2281,6 +2295,28 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
},
wantErr: false,
},
+ {
+ name: "retrieve successful expired intent",
+ args: args{
+ CTX,
+ &user.RetrieveIdentityProviderIntentRequest{
+ IdpIntentId: successfulExpiredID,
+ IdpIntentToken: expiredToken,
+ },
+ },
+ wantErr: true,
+ },
+ {
+ name: "retrieve successful consumed intent",
+ args: args{
+ CTX,
+ &user.RetrieveIdentityProviderIntentRequest{
+ IdpIntentId: successfulConsumedID,
+ IdpIntentToken: consumedToken,
+ },
+ },
+ wantErr: true,
+ },
{
name: "retrieve successful oidc intent",
args: args{
@@ -2466,7 +2502,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Saml{
Saml: &user.IDPSAMLAccessInformation{
- Assertion: []byte(" "),
+ Assertion: []byte(fmt.Sprintf(` `, expiryFormatted)),
},
},
IdpId: samlIdpID,
@@ -2504,7 +2540,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Saml{
Saml: &user.IDPSAMLAccessInformation{
- Assertion: []byte(" "),
+ Assertion: []byte(fmt.Sprintf(` `, expiryFormatted)),
},
},
IdpId: samlIdpID,
diff --git a/internal/api/grpc/user/v2beta/user.go b/internal/api/grpc/user/v2beta/user.go
index cf6dfa6304..93afbde0aa 100644
--- a/internal/api/grpc/user/v2beta/user.go
+++ b/internal/api/grpc/user/v2beta/user.go
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"io"
+ "time"
"golang.org/x/text/language"
"google.golang.org/protobuf/types/known/structpb"
@@ -399,14 +400,14 @@ func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredenti
if err != nil {
return nil, err
}
- externalUser, userID, attributes, err := s.ldapLogin(ctx, intentWriteModel.IDPID, ldapCredentials.GetUsername(), ldapCredentials.GetPassword())
+ externalUser, userID, session, err := s.ldapLogin(ctx, intentWriteModel.IDPID, ldapCredentials.GetUsername(), ldapCredentials.GetPassword())
if err != nil {
if err := s.command.FailIDPIntent(ctx, intentWriteModel, err.Error()); err != nil {
return nil, err
}
return nil, err
}
- token, err := s.command.SucceedLDAPIDPIntent(ctx, intentWriteModel, externalUser, userID, attributes)
+ token, err := s.command.SucceedLDAPIDPIntent(ctx, intentWriteModel, externalUser, userID, session)
if err != nil {
return nil, err
}
@@ -444,7 +445,7 @@ func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUse
return "", nil
}
-func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, map[string][]string, error) {
+func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, *ldap.Session, error) {
provider, err := s.command.GetProvider(ctx, idpID, "", "")
if err != nil {
return nil, "", nil, err
@@ -470,7 +471,7 @@ func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string
for _, item := range session.Entry.Attributes {
attributes[item.Name] = item.Values
}
- return externalUser, userID, attributes, nil
+ return externalUser, userID, session, nil
}
func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.RetrieveIdentityProviderIntentRequest) (_ *user.RetrieveIdentityProviderIntentResponse, err error) {
@@ -484,6 +485,9 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.R
if intent.State != domain.IDPIntentStateSucceeded {
return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-nme4gszsvx", "Errors.Intent.NotSucceeded")
}
+ if time.Now().After(intent.ExpiresAt()) {
+ return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-Afb2s", "Errors.Intent.Expired")
+ }
return idpIntentToIDPIntentPb(intent, s.idpAlg)
}
diff --git a/internal/api/idp/idp.go b/internal/api/idp/idp.go
index c3e9586a59..ebf904a395 100644
--- a/internal/api/idp/idp.go
+++ b/internal/api/idp/idp.go
@@ -287,7 +287,7 @@ func (h *Handler) handleACS(w http.ResponseWriter, r *http.Request) {
userID, err := h.checkExternalUser(ctx, intent.IDPID, idpUser.GetID())
logging.WithFields("intent", intent.AggregateID).OnError(err).Error("could not check if idp user already exists")
- token, err := h.commands.SucceedSAMLIDPIntent(ctx, intent, idpUser, userID, session.Assertion)
+ token, err := h.commands.SucceedSAMLIDPIntent(ctx, intent, idpUser, userID, session)
if err != nil {
redirectToFailureURLErr(w, r, intent, zerrors.ThrowInternal(err, "IDP-JdD3g", "Errors.Intent.TokenCreationFailed"))
return
diff --git a/internal/api/idp/idp_test.go b/internal/api/idp/idp_test.go
index 6804a035af..2f64f598a9 100644
--- a/internal/api/idp/idp_test.go
+++ b/internal/api/idp/idp_test.go
@@ -4,6 +4,7 @@ import (
"net/http/httptest"
"net/url"
"testing"
+ "time"
"github.com/stretchr/testify/assert"
@@ -14,11 +15,12 @@ import (
func Test_redirectToSuccessURL(t *testing.T) {
type args struct {
- id string
- userID string
- token string
- failureURL string
- successURL string
+ id string
+ userID string
+ token string
+ failureURL string
+ successURL string
+ maxIdPIntentLifetime time.Duration
}
type res struct {
want string
@@ -59,7 +61,7 @@ func Test_redirectToSuccessURL(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com", nil)
resp := httptest.NewRecorder()
- wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id)
+ wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id, tt.args.maxIdPIntentLifetime)
wm.FailureURL, _ = url.Parse(tt.args.failureURL)
wm.SuccessURL, _ = url.Parse(tt.args.successURL)
@@ -71,11 +73,12 @@ func Test_redirectToSuccessURL(t *testing.T) {
func Test_redirectToFailureURL(t *testing.T) {
type args struct {
- id string
- failureURL string
- successURL string
- err string
- desc string
+ id string
+ failureURL string
+ successURL string
+ err string
+ desc string
+ maxIdPIntentLifetime time.Duration
}
type res struct {
want string
@@ -115,7 +118,7 @@ func Test_redirectToFailureURL(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com", nil)
resp := httptest.NewRecorder()
- wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id)
+ wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id, tt.args.maxIdPIntentLifetime)
wm.FailureURL, _ = url.Parse(tt.args.failureURL)
wm.SuccessURL, _ = url.Parse(tt.args.successURL)
@@ -127,10 +130,11 @@ func Test_redirectToFailureURL(t *testing.T) {
func Test_redirectToFailureURLErr(t *testing.T) {
type args struct {
- id string
- failureURL string
- successURL string
- err error
+ id string
+ failureURL string
+ successURL string
+ err error
+ maxIdPIntentLifetime time.Duration
}
type res struct {
want string
@@ -158,7 +162,7 @@ func Test_redirectToFailureURLErr(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com", nil)
resp := httptest.NewRecorder()
- wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id)
+ wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id, tt.args.maxIdPIntentLifetime)
wm.FailureURL, _ = url.Parse(tt.args.failureURL)
wm.SuccessURL, _ = url.Parse(tt.args.successURL)
diff --git a/internal/command/command.go b/internal/command/command.go
index b0e67ad52e..64b7b53b67 100644
--- a/internal/command/command.go
+++ b/internal/command/command.go
@@ -81,6 +81,7 @@ type Commands struct {
publicKeyLifetime time.Duration
certificateLifetime time.Duration
defaultSecretGenerators *SecretGenerators
+ maxIdPIntentLifetime time.Duration
samlCertificateAndKeyGenerator func(id string) ([]byte, []byte, error)
webKeyGenerator func(keyID string, alg crypto.EncryptionAlgorithm, genConfig crypto.WebKeyConfig) (encryptedPrivate *crypto.CryptoValue, public *jose.JSONWebKey, err error)
@@ -152,6 +153,7 @@ func StartCommands(
privateKeyLifetime: defaults.KeyConfig.PrivateKeyLifetime,
publicKeyLifetime: defaults.KeyConfig.PublicKeyLifetime,
certificateLifetime: defaults.KeyConfig.CertificateLifetime,
+ maxIdPIntentLifetime: defaults.MaxIdPIntentLifetime,
idpConfigEncryption: idpConfigEncryption,
smtpEncryption: smtpEncryption,
smsEncryption: smsEncryption,
diff --git a/internal/command/idp_intent.go b/internal/command/idp_intent.go
index 3cd9991679..9690117edd 100644
--- a/internal/command/idp_intent.go
+++ b/internal/command/idp_intent.go
@@ -7,7 +7,6 @@ import (
"encoding/xml"
"net/url"
- "github.com/crewjam/saml"
"github.com/crewjam/saml/samlsp"
"github.com/zitadel/oidc/v3/pkg/oidc"
@@ -19,8 +18,10 @@ import (
"github.com/zitadel/zitadel/internal/idp/providers/apple"
"github.com/zitadel/zitadel/internal/idp/providers/azuread"
"github.com/zitadel/zitadel/internal/idp/providers/jwt"
+ "github.com/zitadel/zitadel/internal/idp/providers/ldap"
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
+ "github.com/zitadel/zitadel/internal/idp/providers/saml"
"github.com/zitadel/zitadel/internal/repository/idpintent"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -68,7 +69,7 @@ func (c *Commands) CreateIntent(ctx context.Context, intentID, idpID, successURL
return nil, nil, err
}
}
- writeModel := NewIDPIntentWriteModel(intentID, resourceOwner)
+ writeModel := NewIDPIntentWriteModel(intentID, resourceOwner, c.maxIdPIntentLifetime)
//nolint: staticcheck
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareCreateIntent(writeModel, idpID, successURL, failureURL, idpArguments))
@@ -180,6 +181,7 @@ func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWr
userID,
accessToken,
idToken,
+ idpSession.ExpiresAt(),
)
err = c.pushAppendAndReduce(ctx, writeModel, cmd)
if err != nil {
@@ -188,7 +190,7 @@ func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWr
return token, nil
}
-func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, idpUser idp.User, userID string, assertion *saml.Assertion) (string, error) {
+func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, idpUser idp.User, userID string, session *saml.Session) (string, error) {
token, err := c.generateIntentToken(writeModel.AggregateID)
if err != nil {
return "", err
@@ -197,7 +199,7 @@ func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPInte
if err != nil {
return "", err
}
- assertionData, err := xml.Marshal(assertion)
+ assertionData, err := xml.Marshal(session.Assertion)
if err != nil {
return "", err
}
@@ -213,6 +215,7 @@ func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPInte
idpUser.GetPreferredUsername(),
userID,
assertionEnc,
+ session.ExpiresAt(),
)
err = c.pushAppendAndReduce(ctx, writeModel, cmd)
if err != nil {
@@ -237,7 +240,7 @@ func (c *Commands) generateIntentToken(intentID string) (string, error) {
return base64.RawURLEncoding.EncodeToString(token), nil
}
-func (c *Commands) SucceedLDAPIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, idpUser idp.User, userID string, attributes map[string][]string) (string, error) {
+func (c *Commands) SucceedLDAPIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, idpUser idp.User, userID string, session *ldap.Session) (string, error) {
token, err := c.generateIntentToken(writeModel.AggregateID)
if err != nil {
return "", err
@@ -246,6 +249,10 @@ func (c *Commands) SucceedLDAPIDPIntent(ctx context.Context, writeModel *IDPInte
if err != nil {
return "", err
}
+ attributes := make(map[string][]string, len(session.Entry.Attributes))
+ for _, item := range session.Entry.Attributes {
+ attributes[item.Name] = item.Values
+ }
cmd := idpintent.NewLDAPSucceededEvent(
ctx,
IDPIntentAggregateFromWriteModel(&writeModel.WriteModel),
@@ -254,6 +261,7 @@ func (c *Commands) SucceedLDAPIDPIntent(ctx context.Context, writeModel *IDPInte
idpUser.GetPreferredUsername(),
userID,
attributes,
+ session.ExpiresAt(),
)
err = c.pushAppendAndReduce(ctx, writeModel, cmd)
if err != nil {
@@ -273,7 +281,7 @@ func (c *Commands) FailIDPIntent(ctx context.Context, writeModel *IDPIntentWrite
}
func (c *Commands) GetIntentWriteModel(ctx context.Context, id, resourceOwner string) (*IDPIntentWriteModel, error) {
- writeModel := NewIDPIntentWriteModel(id, resourceOwner)
+ writeModel := NewIDPIntentWriteModel(id, resourceOwner, c.maxIdPIntentLifetime)
err := c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err
diff --git a/internal/command/idp_intent_model.go b/internal/command/idp_intent_model.go
index c6bc26ab06..07e0821813 100644
--- a/internal/command/idp_intent_model.go
+++ b/internal/command/idp_intent_model.go
@@ -2,6 +2,7 @@ package command
import (
"net/url"
+ "time"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
@@ -29,18 +30,29 @@ type IDPIntentWriteModel struct {
RequestID string
Assertion *crypto.CryptoValue
- State domain.IDPIntentState
+ State domain.IDPIntentState
+ succeededAt time.Time
+ maxIdPIntentLifetime time.Duration
+ expiresAt time.Time
}
-func NewIDPIntentWriteModel(id, resourceOwner string) *IDPIntentWriteModel {
+func NewIDPIntentWriteModel(id, resourceOwner string, maxIdPIntentLifetime time.Duration) *IDPIntentWriteModel {
return &IDPIntentWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: id,
ResourceOwner: resourceOwner,
},
+ maxIdPIntentLifetime: maxIdPIntentLifetime,
}
}
+func (wm *IDPIntentWriteModel) ExpiresAt() time.Time {
+ if wm.expiresAt.IsZero() {
+ return wm.succeededAt.Add(wm.maxIdPIntentLifetime)
+ }
+ return wm.expiresAt
+}
+
func (wm *IDPIntentWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
@@ -56,6 +68,8 @@ func (wm *IDPIntentWriteModel) Reduce() error {
wm.reduceLDAPSucceededEvent(e)
case *idpintent.FailedEvent:
wm.reduceFailedEvent(e)
+ case *idpintent.ConsumedEvent:
+ wm.reduceConsumedEvent(e)
}
}
return wm.WriteModel.Reduce()
@@ -74,6 +88,7 @@ func (wm *IDPIntentWriteModel) Query() *eventstore.SearchQueryBuilder {
idpintent.SAMLRequestEventType,
idpintent.LDAPSucceededEventType,
idpintent.FailedEventType,
+ idpintent.ConsumedEventType,
).
Builder()
}
@@ -93,6 +108,8 @@ func (wm *IDPIntentWriteModel) reduceSAMLSucceededEvent(e *idpintent.SAMLSucceed
wm.IDPUserName = e.IDPUserName
wm.Assertion = e.Assertion
wm.State = domain.IDPIntentStateSucceeded
+ wm.succeededAt = e.CreationDate()
+ wm.expiresAt = e.ExpiresAt
}
func (wm *IDPIntentWriteModel) reduceLDAPSucceededEvent(e *idpintent.LDAPSucceededEvent) {
@@ -102,6 +119,8 @@ func (wm *IDPIntentWriteModel) reduceLDAPSucceededEvent(e *idpintent.LDAPSucceed
wm.IDPUserName = e.IDPUserName
wm.IDPEntryAttributes = e.EntryAttributes
wm.State = domain.IDPIntentStateSucceeded
+ wm.succeededAt = e.CreationDate()
+ wm.expiresAt = e.ExpiresAt
}
func (wm *IDPIntentWriteModel) reduceOAuthSucceededEvent(e *idpintent.SucceededEvent) {
@@ -112,6 +131,8 @@ func (wm *IDPIntentWriteModel) reduceOAuthSucceededEvent(e *idpintent.SucceededE
wm.IDPAccessToken = e.IDPAccessToken
wm.IDPIDToken = e.IDPIDToken
wm.State = domain.IDPIntentStateSucceeded
+ wm.succeededAt = e.CreationDate()
+ wm.expiresAt = e.ExpiresAt
}
func (wm *IDPIntentWriteModel) reduceSAMLRequestEvent(e *idpintent.SAMLRequestEvent) {
@@ -122,6 +143,10 @@ func (wm *IDPIntentWriteModel) reduceFailedEvent(e *idpintent.FailedEvent) {
wm.State = domain.IDPIntentStateFailed
}
+func (wm *IDPIntentWriteModel) reduceConsumedEvent(e *idpintent.ConsumedEvent) {
+ wm.State = domain.IDPIntentStateConsumed
+}
+
func IDPIntentAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate {
return &eventstore.Aggregate{
Type: idpintent.AggregateType,
diff --git a/internal/command/idp_intent_test.go b/internal/command/idp_intent_test.go
index 2400b9ee35..1be3971e87 100644
--- a/internal/command/idp_intent_test.go
+++ b/internal/command/idp_intent_test.go
@@ -4,8 +4,10 @@ import (
"context"
"net/url"
"testing"
+ "time"
- "github.com/crewjam/saml"
+ crewjam_saml "github.com/crewjam/saml"
+ goldap "github.com/go-ldap/ldap/v3"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -26,6 +28,7 @@ import (
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
+ "github.com/zitadel/zitadel/internal/idp/providers/saml"
rep_idp "github.com/zitadel/zitadel/internal/repository/idp"
"github.com/zitadel/zitadel/internal/repository/idpintent"
"github.com/zitadel/zitadel/internal/repository/instance"
@@ -867,7 +870,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) {
},
args{
ctx: context.Background(),
- writeModel: NewIDPIntentWriteModel("id", "ro"),
+ writeModel: NewIDPIntentWriteModel("id", "ro", 0),
},
res{
err: zerrors.ThrowInternal(nil, "id", "encryption failed"),
@@ -888,7 +891,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) {
},
args{
ctx: context.Background(),
- writeModel: NewIDPIntentWriteModel("id", "ro"),
+ writeModel: NewIDPIntentWriteModel("id", "ro", 0),
idpSession: &oauth.Session{
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
Token: &oauth2.Token{
@@ -922,6 +925,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) {
Crypted: []byte("accessToken"),
},
"idToken",
+ time.Time{},
)
return event
}(),
@@ -930,7 +934,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) {
},
args{
ctx: context.Background(),
- writeModel: NewIDPIntentWriteModel("id", "instance"),
+ writeModel: NewIDPIntentWriteModel("id", "instance", 0),
idpSession: &openid.Session{
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
Token: &oauth2.Token{
@@ -973,7 +977,7 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) {
ctx context.Context
writeModel *IDPIntentWriteModel
idpUser idp.User
- assertion *saml.Assertion
+ session *saml.Session
userID string
}
type res struct {
@@ -998,7 +1002,7 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) {
},
args{
ctx: context.Background(),
- writeModel: NewIDPIntentWriteModel("id", "ro"),
+ writeModel: NewIDPIntentWriteModel("id", "ro", 0),
},
res{
err: zerrors.ThrowInternal(nil, "id", "encryption failed"),
@@ -1023,14 +1027,17 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) {
KeyID: "id",
Crypted: []byte(" "),
},
+ time.Time{},
),
),
),
},
args{
ctx: context.Background(),
- writeModel: NewIDPIntentWriteModel("id", "instance"),
- assertion: &saml.Assertion{ID: "id"},
+ writeModel: NewIDPIntentWriteModel("id", "instance", 0),
+ session: &saml.Session{
+ Assertion: &crewjam_saml.Assertion{ID: "id"},
+ },
idpUser: openid.NewUser(&oidc.UserInfo{
Subject: "id",
UserInfoProfile: oidc.UserInfoProfile{
@@ -1061,14 +1068,17 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) {
KeyID: "id",
Crypted: []byte(" "),
},
+ time.Time{},
),
),
),
},
args{
ctx: context.Background(),
- writeModel: NewIDPIntentWriteModel("id", "instance"),
- assertion: &saml.Assertion{ID: "id"},
+ writeModel: NewIDPIntentWriteModel("id", "instance", 0),
+ session: &saml.Session{
+ Assertion: &crewjam_saml.Assertion{ID: "id"},
+ },
idpUser: openid.NewUser(&oidc.UserInfo{
Subject: "id",
UserInfoProfile: oidc.UserInfoProfile{
@@ -1088,7 +1098,7 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) {
eventstore: tt.fields.eventstore(t),
idpConfigEncryption: tt.fields.idpConfigEncryption,
}
- got, err := c.SucceedSAMLIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.idpUser, tt.args.userID, tt.args.assertion)
+ got, err := c.SucceedSAMLIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.idpUser, tt.args.userID, tt.args.session)
require.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.token, got)
})
@@ -1128,7 +1138,7 @@ func TestCommands_RequestSAMLIDPIntent(t *testing.T) {
},
args{
ctx: context.Background(),
- writeModel: NewIDPIntentWriteModel("id", "instance"),
+ writeModel: NewIDPIntentWriteModel("id", "instance", 0),
request: "request",
},
res{},
@@ -1156,7 +1166,7 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) {
writeModel *IDPIntentWriteModel
idpUser idp.User
userID string
- attributes map[string][]string
+ session *ldap.Session
}
type res struct {
token string
@@ -1180,7 +1190,7 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) {
},
args{
ctx: context.Background(),
- writeModel: NewIDPIntentWriteModel("id", "instance"),
+ writeModel: NewIDPIntentWriteModel("id", "instance", 0),
},
res{
err: zerrors.ThrowInternal(nil, "id", "encryption failed"),
@@ -1200,14 +1210,24 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) {
"username",
"",
map[string][]string{"id": {"id"}},
+ time.Time{},
),
),
),
},
args{
ctx: context.Background(),
- writeModel: NewIDPIntentWriteModel("id", "instance"),
- attributes: map[string][]string{"id": {"id"}},
+ writeModel: NewIDPIntentWriteModel("id", "instance", 0),
+ session: &ldap.Session{
+ Entry: &goldap.Entry{
+ Attributes: []*goldap.EntryAttribute{
+ {
+ Name: "id",
+ Values: []string{"id"},
+ },
+ },
+ },
+ },
idpUser: ldap.NewUser(
"id",
"",
@@ -1235,7 +1255,7 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) {
eventstore: tt.fields.eventstore(t),
idpConfigEncryption: tt.fields.idpConfigEncryption,
}
- got, err := c.SucceedLDAPIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.idpUser, tt.args.userID, tt.args.attributes)
+ got, err := c.SucceedLDAPIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.idpUser, tt.args.userID, tt.args.session)
require.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.token, got)
})
@@ -1275,7 +1295,7 @@ func TestCommands_FailIDPIntent(t *testing.T) {
},
args{
ctx: context.Background(),
- writeModel: NewIDPIntentWriteModel("id", "instance"),
+ writeModel: NewIDPIntentWriteModel("id", "instance", 0),
reason: "reason",
},
res{
diff --git a/internal/command/session.go b/internal/command/session.go
index d00e541e62..3c06c22967 100644
--- a/internal/command/session.go
+++ b/internal/command/session.go
@@ -17,6 +17,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/notification/senders"
+ "github.com/zitadel/zitadel/internal/repository/idpintent"
"github.com/zitadel/zitadel/internal/repository/session"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
@@ -32,31 +33,33 @@ type SessionCommands struct {
eventstore *eventstore.Eventstore
eventCommands []eventstore.Command
- hasher *crypto.Hasher
- intentAlg crypto.EncryptionAlgorithm
- totpAlg crypto.EncryptionAlgorithm
- otpAlg crypto.EncryptionAlgorithm
- createCode encryptedCodeWithDefaultFunc
- createPhoneCode encryptedCodeGeneratorWithDefaultFunc
- createToken func(sessionID string) (id string, token string, err error)
- getCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error)
- now func() time.Time
+ hasher *crypto.Hasher
+ intentAlg crypto.EncryptionAlgorithm
+ totpAlg crypto.EncryptionAlgorithm
+ otpAlg crypto.EncryptionAlgorithm
+ createCode encryptedCodeWithDefaultFunc
+ createPhoneCode encryptedCodeGeneratorWithDefaultFunc
+ createToken func(sessionID string) (id string, token string, err error)
+ getCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error)
+ now func() time.Time
+ maxIdPIntentLifetime time.Duration
}
func (c *Commands) NewSessionCommands(cmds []SessionCommand, session *SessionWriteModel) *SessionCommands {
return &SessionCommands{
- sessionCommands: cmds,
- sessionWriteModel: session,
- eventstore: c.eventstore,
- hasher: c.userPasswordHasher,
- intentAlg: c.idpConfigEncryption,
- totpAlg: c.multifactors.OTP.CryptoMFA,
- otpAlg: c.userEncryption,
- createCode: c.newEncryptedCodeWithDefault,
- createPhoneCode: c.newPhoneCode,
- createToken: c.sessionTokenCreator,
- getCodeVerifier: c.phoneCodeVerifierFromConfig,
- now: time.Now,
+ sessionCommands: cmds,
+ sessionWriteModel: session,
+ eventstore: c.eventstore,
+ hasher: c.userPasswordHasher,
+ intentAlg: c.idpConfigEncryption,
+ totpAlg: c.multifactors.OTP.CryptoMFA,
+ otpAlg: c.userEncryption,
+ createCode: c.newEncryptedCodeWithDefault,
+ createPhoneCode: c.newPhoneCode,
+ createToken: c.sessionTokenCreator,
+ getCodeVerifier: c.phoneCodeVerifierFromConfig,
+ now: time.Now,
+ maxIdPIntentLifetime: c.maxIdPIntentLifetime,
}
}
@@ -92,7 +95,7 @@ func CheckIntent(intentID, token string) SessionCommand {
if err := crypto.CheckToken(cmd.intentAlg, token, intentID); err != nil {
return nil, err
}
- cmd.intentWriteModel = NewIDPIntentWriteModel(intentID, "")
+ cmd.intentWriteModel = NewIDPIntentWriteModel(intentID, "", cmd.maxIdPIntentLifetime)
err := cmd.eventstore.FilterToQueryReducer(ctx, cmd.intentWriteModel)
if err != nil {
return nil, err
@@ -100,6 +103,9 @@ func CheckIntent(intentID, token string) SessionCommand {
if cmd.intentWriteModel.State != domain.IDPIntentStateSucceeded {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Df4bw", "Errors.Intent.NotSucceeded")
}
+ if time.Now().After(cmd.intentWriteModel.ExpiresAt()) {
+ return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-SAf42", "Errors.Intent.Expired")
+ }
if cmd.intentWriteModel.UserID != "" {
if cmd.intentWriteModel.UserID != cmd.sessionWriteModel.UserID {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-O8xk3w", "Errors.Intent.OtherUser")
@@ -168,6 +174,7 @@ func (s *SessionCommands) PasswordChecked(ctx context.Context, checkedAt time.Ti
func (s *SessionCommands) IntentChecked(ctx context.Context, checkedAt time.Time) {
s.eventCommands = append(s.eventCommands, session.NewIntentCheckedEvent(ctx, s.sessionWriteModel.aggregate, checkedAt))
+ s.eventCommands = append(s.eventCommands, idpintent.NewConsumedEvent(ctx, IDPIntentAggregateFromWriteModel(&s.intentWriteModel.WriteModel)))
}
func (s *SessionCommands) WebAuthNChallenged(ctx context.Context, challenge string, allowedCrentialIDs [][]byte, userVerification domain.UserVerificationRequirement, rpid string) {
diff --git a/internal/command/session_test.go b/internal/command/session_test.go
index 60027d3a05..e65f32fb57 100644
--- a/internal/command/session_test.go
+++ b/internal/command/session_test.go
@@ -695,6 +695,7 @@ func TestCommands_updateSession(t *testing.T) {
"userID2",
nil,
"",
+ time.Now().Add(time.Hour),
),
),
),
@@ -757,6 +758,111 @@ func TestCommands_updateSession(t *testing.T) {
err: zerrors.ThrowPermissionDenied(nil, "CRYPTO-CRYPTO", "Errors.Intent.InvalidToken"),
},
},
+ {
+ "set user, intent token already consumed",
+ fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
+ "username", "", "", "", "", language.English, domain.GenderUnspecified, "", false),
+ ),
+ eventFromEventPusher(
+ idpintent.NewSucceededEvent(context.Background(),
+ &idpintent.NewAggregate("intent", "instance1").Aggregate,
+ nil,
+ "idpUserID",
+ "idpUsername",
+ "userID",
+ nil,
+ "",
+ time.Now().Add(time.Hour),
+ ),
+ ),
+ eventFromEventPusher(
+ idpintent.NewConsumedEvent(context.Background(),
+ &idpintent.NewAggregate("intent", "instance1").Aggregate,
+ ),
+ ),
+ ),
+ ),
+ },
+ args{
+ ctx: authz.NewMockContext("instance1", "", ""),
+ checks: &SessionCommands{
+ sessionWriteModel: NewSessionWriteModel("sessionID", "instance1"),
+ sessionCommands: []SessionCommand{
+ CheckUser("userID", "org1", &language.Afrikaans),
+ CheckIntent("intent", "aW50ZW50"),
+ },
+ createToken: func(sessionID string) (string, string, error) {
+ return "tokenID",
+ "token",
+ nil
+ },
+ intentAlg: decryption(nil),
+ now: func() time.Time {
+ return testNow
+ },
+ },
+ metadata: map[string][]byte{
+ "key": []byte("value"),
+ },
+ },
+ res{
+ err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Df4bw", "Errors.Intent.NotSucceeded"),
+ },
+ },
+ {
+ "set user, intent token already expired",
+ fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
+ "username", "", "", "", "", language.English, domain.GenderUnspecified, "", false),
+ ),
+ eventFromEventPusher(
+ idpintent.NewSucceededEvent(context.Background(),
+ &idpintent.NewAggregate("intent", "instance1").Aggregate,
+ nil,
+ "idpUserID",
+ "idpUsername",
+ "userID",
+ nil,
+ "",
+ time.Now().Add(-time.Hour),
+ ),
+ ),
+ ),
+ ),
+ },
+ args{
+ ctx: authz.NewMockContext("instance1", "", ""),
+ checks: &SessionCommands{
+ sessionWriteModel: NewSessionWriteModel("sessionID", "instance1"),
+ sessionCommands: []SessionCommand{
+ CheckUser("userID", "org1", &language.Afrikaans),
+ CheckIntent("intent", "aW50ZW50"),
+ },
+ createToken: func(sessionID string) (string, string, error) {
+ return "tokenID",
+ "token",
+ nil
+ },
+ intentAlg: decryption(nil),
+ now: func() time.Time {
+ return testNow
+ },
+ },
+ metadata: map[string][]byte{
+ "key": []byte("value"),
+ },
+ },
+ res{
+ err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-SAf42", "Errors.Intent.Expired"),
+ },
+ },
{
"set user, intent, metadata and token",
fields{
@@ -768,13 +874,14 @@ func TestCommands_updateSession(t *testing.T) {
),
eventFromEventPusher(
idpintent.NewSucceededEvent(context.Background(),
- &idpintent.NewAggregate("id", "instance1").Aggregate,
+ &idpintent.NewAggregate("intent", "instance1").Aggregate,
nil,
"idpUserID",
"idpUsername",
"userID",
nil,
"",
+ time.Now().Add(time.Hour),
),
),
),
@@ -783,6 +890,7 @@ func TestCommands_updateSession(t *testing.T) {
"userID", "org1", testNow, &language.Afrikaans),
session.NewIntentCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate,
testNow),
+ idpintent.NewConsumedEvent(context.Background(), &idpintent.NewAggregate("intent", "org1").Aggregate),
session.NewMetadataSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate,
map[string][]byte{"key": []byte("value")}),
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate,
@@ -842,13 +950,14 @@ func TestCommands_updateSession(t *testing.T) {
),
eventFromEventPusher(
idpintent.NewSucceededEvent(context.Background(),
- &idpintent.NewAggregate("id", "instance1").Aggregate,
+ &idpintent.NewAggregate("intent", "instance1").Aggregate,
nil,
"idpUserID",
"idpUsername",
"",
nil,
"",
+ time.Now().Add(time.Hour),
),
),
),
@@ -866,6 +975,7 @@ func TestCommands_updateSession(t *testing.T) {
"userID", "org1", testNow, &language.Afrikaans),
session.NewIntentCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate,
testNow),
+ idpintent.NewConsumedEvent(context.Background(), &idpintent.NewAggregate("intent", "org1").Aggregate),
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate,
"tokenID"),
),
diff --git a/internal/config/systemdefaults/system_defaults.go b/internal/config/systemdefaults/system_defaults.go
index f6d39befe7..827dd61f73 100644
--- a/internal/config/systemdefaults/system_defaults.go
+++ b/internal/config/systemdefaults/system_defaults.go
@@ -7,15 +7,16 @@ import (
)
type SystemDefaults struct {
- SecretGenerators SecretGenerators
- PasswordHasher crypto.HashConfig
- SecretHasher crypto.HashConfig
- Multifactors MultifactorConfig
- DomainVerification DomainVerification
- Notifications Notifications
- KeyConfig KeyConfig
- DefaultQueryLimit uint64
- MaxQueryLimit uint64
+ SecretGenerators SecretGenerators
+ PasswordHasher crypto.HashConfig
+ SecretHasher crypto.HashConfig
+ Multifactors MultifactorConfig
+ DomainVerification DomainVerification
+ Notifications Notifications
+ KeyConfig KeyConfig
+ DefaultQueryLimit uint64
+ MaxQueryLimit uint64
+ MaxIdPIntentLifetime time.Duration
}
type SecretGenerators struct {
diff --git a/internal/domain/idp.go b/internal/domain/idp.go
index e2571f6b0d..bea106298b 100644
--- a/internal/domain/idp.go
+++ b/internal/domain/idp.go
@@ -115,6 +115,7 @@ const (
IDPIntentStateStarted
IDPIntentStateSucceeded
IDPIntentStateFailed
+ IDPIntentStateConsumed
idpIntentStateCount
)
diff --git a/internal/idp/providers/apple/session.go b/internal/idp/providers/apple/session.go
index eee68fa2a5..9395d84b2b 100644
--- a/internal/idp/providers/apple/session.go
+++ b/internal/idp/providers/apple/session.go
@@ -10,6 +10,8 @@ import (
"github.com/zitadel/zitadel/internal/idp/providers/oidc"
)
+var _ idp.Session = (*Session)(nil)
+
// Session extends the [oidc.Session] with the formValues returned from the callback.
// This enables to parse the user (name and email), which Apple only returns as form params on registration
type Session struct {
diff --git a/internal/idp/providers/azuread/session.go b/internal/idp/providers/azuread/session.go
index 4b0a6fb844..169784fb58 100644
--- a/internal/idp/providers/azuread/session.go
+++ b/internal/idp/providers/azuread/session.go
@@ -3,6 +3,7 @@ package azuread
import (
"context"
"net/http"
+ "time"
"github.com/zitadel/oidc/v3/pkg/client/rp"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
@@ -12,6 +13,8 @@ import (
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
)
+var _ idp.Session = (*Session)(nil)
+
// Session extends the [oauth.Session] to be able to handle the id_token and to implement the [idp.SessionSupportsMigration] functionality
type Session struct {
*Provider
@@ -79,6 +82,13 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) {
return user, nil
}
+func (s *Session) ExpiresAt() time.Time {
+ if s.OAuthSession == nil {
+ return time.Time{}
+ }
+ return s.OAuthSession.ExpiresAt()
+}
+
// Tokens returns the [oidc.Tokens] of the underlying [oauth.Session].
func (s *Session) Tokens() *oidc.Tokens[*oidc.IDTokenClaims] {
return s.oauth().Tokens
diff --git a/internal/idp/providers/jwt/session.go b/internal/idp/providers/jwt/session.go
index 6df08a6998..5138812f3c 100644
--- a/internal/idp/providers/jwt/session.go
+++ b/internal/idp/providers/jwt/session.go
@@ -57,6 +57,13 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) {
return &User{s.Tokens.IDTokenClaims}, nil
}
+func (s *Session) ExpiresAt() time.Time {
+ if s.Tokens == nil || s.Tokens.IDTokenClaims == nil {
+ return time.Time{}
+ }
+ return s.Tokens.IDTokenClaims.GetExpiration()
+}
+
func (s *Session) validateToken(ctx context.Context, token string) (*oidc.IDTokenClaims, error) {
logging.Debug("begin token validation")
// TODO: be able to specify them in the template: https://github.com/zitadel/zitadel/issues/5322
diff --git a/internal/idp/providers/ldap/session.go b/internal/idp/providers/ldap/session.go
index 0a6a87ba3d..1679e35b61 100644
--- a/internal/idp/providers/ldap/session.go
+++ b/internal/idp/providers/ldap/session.go
@@ -96,6 +96,10 @@ func (s *Session) FetchUser(_ context.Context) (_ idp.User, err error) {
)
}
+func (s *Session) ExpiresAt() time.Time {
+ return time.Time{} // falls back to the default expiration time
+}
+
func tryBind(
server string,
startTLS bool,
diff --git a/internal/idp/providers/oauth/session.go b/internal/idp/providers/oauth/session.go
index 247a7f8710..c9e175d1cf 100644
--- a/internal/idp/providers/oauth/session.go
+++ b/internal/idp/providers/oauth/session.go
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"net/http"
+ "time"
"github.com/zitadel/oidc/v3/pkg/client/rp"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
@@ -69,6 +70,13 @@ func (s *Session) FetchUser(ctx context.Context) (_ idp.User, err error) {
return user, nil
}
+func (s *Session) ExpiresAt() time.Time {
+ if s.Tokens == nil {
+ return time.Time{}
+ }
+ return s.Tokens.Expiry
+}
+
func (s *Session) authorize(ctx context.Context) (err error) {
if s.Code == "" {
return ErrCodeMissing
diff --git a/internal/idp/providers/oidc/session.go b/internal/idp/providers/oidc/session.go
index b17a3b0a0b..430a14e5bb 100644
--- a/internal/idp/providers/oidc/session.go
+++ b/internal/idp/providers/oidc/session.go
@@ -3,6 +3,7 @@ package oidc
import (
"context"
"errors"
+ "time"
"github.com/zitadel/oidc/v3/pkg/client/rp"
"github.com/zitadel/oidc/v3/pkg/oidc"
@@ -72,6 +73,13 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) {
return u, nil
}
+func (s *Session) ExpiresAt() time.Time {
+ if s.Tokens == nil {
+ return time.Time{}
+ }
+ return s.Tokens.Expiry
+}
+
func (s *Session) Authorize(ctx context.Context) (err error) {
if s.Code == "" {
return ErrCodeMissing
diff --git a/internal/idp/providers/saml/session.go b/internal/idp/providers/saml/session.go
index b0748d33a3..e2a1655a26 100644
--- a/internal/idp/providers/saml/session.go
+++ b/internal/idp/providers/saml/session.go
@@ -6,6 +6,7 @@ import (
"errors"
"net/http"
"net/url"
+ "time"
"github.com/crewjam/saml"
"github.com/crewjam/saml/samlsp"
@@ -107,6 +108,13 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) {
return userMapper, nil
}
+func (s *Session) ExpiresAt() time.Time {
+ if s.Assertion == nil || s.Assertion.Conditions == nil {
+ return time.Time{}
+ }
+ return s.Assertion.Conditions.NotOnOrAfter
+}
+
func (s *Session) transientMappingID() (string, error) {
for _, statement := range s.Assertion.AttributeStatements {
for _, attribute := range statement.Attributes {
diff --git a/internal/idp/session.go b/internal/idp/session.go
index ab54bcabaa..fc593eb820 100644
--- a/internal/idp/session.go
+++ b/internal/idp/session.go
@@ -2,6 +2,7 @@ package idp
import (
"context"
+ "time"
)
// Session is the minimal implementation for a session of a 3rd party authentication [Provider]
@@ -9,6 +10,7 @@ type Session interface {
GetAuth(ctx context.Context) (content string, redirect bool)
PersistentParameters() map[string]any
FetchUser(ctx context.Context) (User, error)
+ ExpiresAt() time.Time
}
// SessionSupportsMigration is an optional extension to the Session interface.
diff --git a/internal/integration/client.go b/internal/integration/client.go
index e82a6bec55..f1bcfb41bd 100644
--- a/internal/integration/client.go
+++ b/internal/integration/client.go
@@ -672,6 +672,23 @@ func (i *Instance) CreatePasswordSession(t *testing.T, ctx context.Context, user
createResp.GetDetails().GetChangeDate().AsTime(), createResp.GetDetails().GetChangeDate().AsTime()
}
+func (i *Instance) CreateIntentSession(t *testing.T, ctx context.Context, userID, intentID, intentToken string) (id, token string, start, change time.Time) {
+ createResp, err := i.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{
+ Checks: &session.Checks{
+ User: &session.CheckUser{
+ Search: &session.CheckUser_UserId{UserId: userID},
+ },
+ IdpIntent: &session.CheckIDPIntent{
+ IdpIntentId: intentID,
+ IdpIntentToken: intentToken,
+ },
+ },
+ })
+ require.NoError(t, err)
+ return createResp.GetSessionId(), createResp.GetSessionToken(),
+ createResp.GetDetails().GetChangeDate().AsTime(), createResp.GetDetails().GetChangeDate().AsTime()
+}
+
func (i *Instance) CreateProjectGrant(ctx context.Context, projectID, grantedOrgID string) *mgmt.AddProjectGrantResponse {
resp, err := i.Client.Mgmt.AddProjectGrant(ctx, &mgmt.AddProjectGrantRequest{
GrantedOrgId: grantedOrgID,
diff --git a/internal/integration/sink/server.go b/internal/integration/sink/server.go
index 633ebf424f..8abb31a63e 100644
--- a/internal/integration/sink/server.go
+++ b/internal/integration/sink/server.go
@@ -17,6 +17,7 @@ import (
crewjam_saml "github.com/crewjam/saml"
"github.com/go-chi/chi/v5"
+ goldap "github.com/go-ldap/ldap/v3"
"github.com/gorilla/websocket"
"github.com/sirupsen/logrus"
"github.com/zitadel/logging"
@@ -48,7 +49,7 @@ func CallURL(ch Channel) string {
return u.String()
}
-func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string) (string, string, time.Time, uint64, error) {
+func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (string, string, time.Time, uint64, error) {
u := url.URL{
Scheme: "http",
Host: host,
@@ -59,6 +60,7 @@ func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string) (string,
IDPID: idpID,
IDPUserID: idpUserID,
UserID: userID,
+ Expiry: expiry,
})
if err != nil {
return "", "", time.Time{}, uint64(0), err
@@ -66,7 +68,7 @@ func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string) (string,
return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil
}
-func SuccessfulOIDCIntent(instanceID, idpID, idpUserID, userID string) (string, string, time.Time, uint64, error) {
+func SuccessfulOIDCIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (string, string, time.Time, uint64, error) {
u := url.URL{
Scheme: "http",
Host: host,
@@ -77,6 +79,7 @@ func SuccessfulOIDCIntent(instanceID, idpID, idpUserID, userID string) (string,
IDPID: idpID,
IDPUserID: idpUserID,
UserID: userID,
+ Expiry: expiry,
})
if err != nil {
return "", "", time.Time{}, uint64(0), err
@@ -84,7 +87,7 @@ func SuccessfulOIDCIntent(instanceID, idpID, idpUserID, userID string) (string,
return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil
}
-func SuccessfulSAMLIntent(instanceID, idpID, idpUserID, userID string) (string, string, time.Time, uint64, error) {
+func SuccessfulSAMLIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (string, string, time.Time, uint64, error) {
u := url.URL{
Scheme: "http",
Host: host,
@@ -95,6 +98,7 @@ func SuccessfulSAMLIntent(instanceID, idpID, idpUserID, userID string) (string,
IDPID: idpID,
IDPUserID: idpUserID,
UserID: userID,
+ Expiry: expiry,
})
if err != nil {
return "", "", time.Time{}, uint64(0), err
@@ -282,10 +286,11 @@ func readLoop(ws *websocket.Conn) (done chan error) {
}
type SuccessfulIntentRequest struct {
- InstanceID string `json:"instance_id"`
- IDPID string `json:"idp_id"`
- IDPUserID string `json:"idp_user_id"`
- UserID string `json:"user_id"`
+ InstanceID string `json:"instance_id"`
+ IDPID string `json:"idp_id"`
+ IDPUserID string `json:"idp_user_id"`
+ UserID string `json:"user_id"`
+ Expiry time.Time `json:"expiry"`
}
type SuccessfulIntentResponse struct {
IntentID string `json:"intent_id"`
@@ -376,6 +381,7 @@ func createSuccessfulOAuthIntent(ctx context.Context, cmd *command.Commands, req
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
Token: &oauth2.Token{
AccessToken: "accessToken",
+ Expiry: req.Expiry,
},
IDToken: "idToken",
},
@@ -407,6 +413,7 @@ func createSuccessfulOIDCIntent(ctx context.Context, cmd *command.Commands, req
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
Token: &oauth2.Token{
AccessToken: "accessToken",
+ Expiry: req.Expiry,
},
IDToken: "idToken",
},
@@ -431,9 +438,16 @@ func createSuccessfulSAMLIntent(ctx context.Context, cmd *command.Commands, req
ID: req.IDPUserID,
Attributes: map[string][]string{"attribute1": {"value1"}},
}
- assertion := &crewjam_saml.Assertion{ID: "id"}
+ session := &saml.Session{
+ Assertion: &crewjam_saml.Assertion{
+ ID: "id",
+ Conditions: &crewjam_saml.Conditions{
+ NotOnOrAfter: req.Expiry,
+ },
+ },
+ }
- token, err := cmd.SucceedSAMLIDPIntent(ctx, writeModel, idpUser, req.UserID, assertion)
+ token, err := cmd.SucceedSAMLIDPIntent(ctx, writeModel, idpUser, req.UserID, session)
if err != nil {
return nil, err
}
@@ -465,8 +479,14 @@ func createSuccessfulLDAPIntent(ctx context.Context, cmd *command.Commands, req
"",
"",
)
- attributes := map[string][]string{"id": {req.IDPUserID}, "username": {username}, "language": {lang.String()}}
- token, err := cmd.SucceedLDAPIDPIntent(ctx, writeModel, idpUser, req.UserID, attributes)
+ session := &ldap.Session{Entry: &goldap.Entry{
+ Attributes: []*goldap.EntryAttribute{
+ {Name: "id", Values: []string{req.IDPUserID}},
+ {Name: "username", Values: []string{username}},
+ {Name: "language", Values: []string{lang.String()}},
+ },
+ }}
+ token, err := cmd.SucceedLDAPIDPIntent(ctx, writeModel, idpUser, req.UserID, session)
if err != nil {
return nil, err
}
diff --git a/internal/repository/idpintent/eventstore.go b/internal/repository/idpintent/eventstore.go
index ea94803973..6bec32c735 100644
--- a/internal/repository/idpintent/eventstore.go
+++ b/internal/repository/idpintent/eventstore.go
@@ -11,4 +11,5 @@ func init() {
eventstore.RegisterFilterEventMapper(AggregateType, SAMLRequestEventType, SAMLRequestEventMapper)
eventstore.RegisterFilterEventMapper(AggregateType, LDAPSucceededEventType, LDAPSucceededEventMapper)
eventstore.RegisterFilterEventMapper(AggregateType, FailedEventType, FailedEventMapper)
+ eventstore.RegisterFilterEventMapper(AggregateType, ConsumedEventType, eventstore.GenericEventMapper[ConsumedEvent])
}
diff --git a/internal/repository/idpintent/intent.go b/internal/repository/idpintent/intent.go
index 27e6391f95..e4ee28cae9 100644
--- a/internal/repository/idpintent/intent.go
+++ b/internal/repository/idpintent/intent.go
@@ -3,6 +3,7 @@ package idpintent
import (
"context"
"net/url"
+ "time"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/eventstore"
@@ -16,6 +17,7 @@ const (
SAMLRequestEventType = instanceEventTypePrefix + "saml.requested"
LDAPSucceededEventType = instanceEventTypePrefix + "ldap.succeeded"
FailedEventType = instanceEventTypePrefix + "failed"
+ ConsumedEventType = instanceEventTypePrefix + "consumed"
)
type StartedEvent struct {
@@ -79,6 +81,7 @@ type SucceededEvent struct {
IDPAccessToken *crypto.CryptoValue `json:"idpAccessToken,omitempty"`
IDPIDToken string `json:"idpIdToken,omitempty"`
+ ExpiresAt time.Time `json:"expiresAt,omitempty"`
}
func NewSucceededEvent(
@@ -90,6 +93,7 @@ func NewSucceededEvent(
userID string,
idpAccessToken *crypto.CryptoValue,
idpIDToken string,
+ expiresAt time.Time,
) *SucceededEvent {
return &SucceededEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
@@ -103,6 +107,7 @@ func NewSucceededEvent(
UserID: userID,
IDPAccessToken: idpAccessToken,
IDPIDToken: idpIDToken,
+ ExpiresAt: expiresAt,
}
}
@@ -136,6 +141,7 @@ type SAMLSucceededEvent struct {
UserID string `json:"userId,omitempty"`
Assertion *crypto.CryptoValue `json:"assertion,omitempty"`
+ ExpiresAt time.Time `json:"expiresAt,omitempty"`
}
func NewSAMLSucceededEvent(
@@ -146,6 +152,7 @@ func NewSAMLSucceededEvent(
idpUserName,
userID string,
assertion *crypto.CryptoValue,
+ expiresAt time.Time,
) *SAMLSucceededEvent {
return &SAMLSucceededEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
@@ -158,6 +165,7 @@ func NewSAMLSucceededEvent(
IDPUserName: idpUserName,
UserID: userID,
Assertion: assertion,
+ ExpiresAt: expiresAt,
}
}
@@ -233,6 +241,7 @@ type LDAPSucceededEvent struct {
UserID string `json:"userId,omitempty"`
EntryAttributes map[string][]string `json:"user,omitempty"`
+ ExpiresAt time.Time `json:"expiresAt,omitempty"`
}
func NewLDAPSucceededEvent(
@@ -243,6 +252,7 @@ func NewLDAPSucceededEvent(
idpUserName,
userID string,
attributes map[string][]string,
+ expiresAt time.Time,
) *LDAPSucceededEvent {
return &LDAPSucceededEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
@@ -255,6 +265,7 @@ func NewLDAPSucceededEvent(
IDPUserName: idpUserName,
UserID: userID,
EntryAttributes: attributes,
+ ExpiresAt: expiresAt,
}
}
@@ -320,3 +331,32 @@ func FailedEventMapper(event eventstore.Event) (eventstore.Event, error) {
return e, nil
}
+
+type ConsumedEvent struct {
+ eventstore.BaseEvent `json:"-"`
+}
+
+func NewConsumedEvent(
+ ctx context.Context,
+ aggregate *eventstore.Aggregate,
+) *ConsumedEvent {
+ return &ConsumedEvent{
+ BaseEvent: *eventstore.NewBaseEventForPush(
+ ctx,
+ aggregate,
+ ConsumedEventType,
+ ),
+ }
+}
+
+func (e *ConsumedEvent) Payload() interface{} {
+ return e
+}
+
+func (e *ConsumedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
+ return nil
+}
+
+func (e *ConsumedEvent) SetBaseEvent(base *eventstore.BaseEvent) {
+ e.BaseEvent = *base
+}
diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml
index d7dc18898b..8254b82b45 100644
--- a/internal/static/i18n/bg.yaml
+++ b/internal/static/i18n/bg.yaml
@@ -554,6 +554,7 @@ Errors:
StateMissing: В заявката липсва параметър състояние
NotStarted: Намерението не е стартирано или вече е прекратено
NotSucceeded: Намерението не е успешно
+ Expired: Намерението е изтекло
TokenCreationFailed: Неуспешно създаване на токен
InvalidToken: Знакът за намерение е невалиден
OtherUser: Намерение, предназначено за друг потребител
diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml
index 80db4952f9..bb4172fbff 100644
--- a/internal/static/i18n/cs.yaml
+++ b/internal/static/i18n/cs.yaml
@@ -534,6 +534,7 @@ Errors:
StateMissing: V požadavku chybí parametr stavu
NotStarted: Záměr nebyl zahájen nebo již byl ukončen
NotSucceeded: Záměr nebyl úspěšný
+ Expired: Záměr vypršel
TokenCreationFailed: Vytvoření tokenu selhalo
InvalidToken: Token záměru je neplatný
OtherUser: Záměr určený pro jiného uživatele
diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml
index dcb3ac5c71..a24ce7c933 100644
--- a/internal/static/i18n/de.yaml
+++ b/internal/static/i18n/de.yaml
@@ -536,6 +536,7 @@ Errors:
StateMissing: State parameter fehlt im Request
NotStarted: Intent wurde nicht gestartet oder wurde bereits beendet
NotSucceeded: Intent war nicht erfolgreich
+ Expired: Intent ist abgelaufen
TokenCreationFailed: Tokenerstellung schlug fehl
InvalidToken: Intent Token ist ungültig
OtherUser: Intent ist für anderen Benutzer gedacht
diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml
index bd8d26d727..e8f2781de1 100644
--- a/internal/static/i18n/en.yaml
+++ b/internal/static/i18n/en.yaml
@@ -537,6 +537,7 @@ Errors:
StateMissing: State parameter is missing in the request
NotStarted: Intent is not started or was already terminated
NotSucceeded: Intent has not succeeded
+ Expired: Intent has expired
TokenCreationFailed: Token creation failed
InvalidToken: Intent Token is invalid
OtherUser: Intent meant for another user
diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml
index 9f11b63964..b91d055f70 100644
--- a/internal/static/i18n/es.yaml
+++ b/internal/static/i18n/es.yaml
@@ -536,6 +536,7 @@ Errors:
StateMissing: Falta un parámetro de estado en la solicitud
NotStarted: La intención no se ha iniciado o ya ha finalizado
NotSucceeded: Intento fallido
+ Expired: La intención ha expirado
TokenCreationFailed: Fallo en la creación del token
InvalidToken: El token de la intención no es válido
OtherUser: Destinado a otro usuario
diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml
index ff8393befc..98f2bee9a0 100644
--- a/internal/static/i18n/fr.yaml
+++ b/internal/static/i18n/fr.yaml
@@ -536,6 +536,7 @@ Errors:
StateMissing: Paramètre d'état manquant dans la requête
NotStarted: Intent n'a pas démarré ou s'est déjà terminé
NotSucceeded: l'intention n'a pas abouti
+ Expired: L'intention a expiré
TokenCreationFailed: La création du token a échoué
InvalidToken: Le jeton d'intention n'est pas valide
OtherUser: Intention destinée à un autre utilisateur
diff --git a/internal/static/i18n/hu.yaml b/internal/static/i18n/hu.yaml
index b17c6a1225..5becd6e606 100644
--- a/internal/static/i18n/hu.yaml
+++ b/internal/static/i18n/hu.yaml
@@ -536,6 +536,7 @@ Errors:
StateMissing: A kérésből hiányzik a State paraméter
NotStarted: Az intent nem indult el, vagy már befejeződött
NotSucceeded: Az intent nem sikerült
+ Expired: A kérésből lejárt
TokenCreationFailed: A token létrehozása nem sikerült
InvalidToken: Az Intent Token érvénytelen
OtherUser: Az intent egy másik felhasználónak szól
diff --git a/internal/static/i18n/id.yaml b/internal/static/i18n/id.yaml
index 56a454e71d..0108d7618b 100644
--- a/internal/static/i18n/id.yaml
+++ b/internal/static/i18n/id.yaml
@@ -536,6 +536,7 @@ Errors:
StateMissing: Parameter status tidak ada dalam permintaan
NotStarted: Niat belum dimulai atau sudah dihentikan
NotSucceeded: Niatnya belum berhasil
+ Expired: Kode sudah habis masa berlakunya
TokenCreationFailed: Pembuatan token gagal
InvalidToken: Token Niat tidak valid
OtherUser: Maksudnya ditujukan untuk pengguna lain
diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml
index 6713abf2e1..750c48471a 100644
--- a/internal/static/i18n/it.yaml
+++ b/internal/static/i18n/it.yaml
@@ -536,6 +536,7 @@ Errors:
StateMissing: parametro di stato mancante nella richiesta
NotStarted: l'intento non è stato avviato o è già stato terminato
NotSucceeded: l'intento non è andato a buon fine
+ Expired: L'intento è scaduto
TokenCreationFailed: creazione del token fallita
InvalidToken: Il token dell'intento non è valido
OtherUser: Intento destinato a un altro utente
diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml
index f57d0f6661..fcd7920999 100644
--- a/internal/static/i18n/ja.yaml
+++ b/internal/static/i18n/ja.yaml
@@ -537,6 +537,7 @@ Errors:
StateMissing: リクエストに State パラメータがありません
NotStarted: インテントが開始されなかったか、既に終了している
NotSucceeded: インテントが成功しなかった
+ Expired: 意図の有効期限が切れました
TokenCreationFailed: トークンの作成に失敗しました
InvalidToken: インテントのトークンが無効である
OtherUser: 他のユーザーを意図している
diff --git a/internal/static/i18n/ko.yaml b/internal/static/i18n/ko.yaml
index d238142e01..d83af62235 100644
--- a/internal/static/i18n/ko.yaml
+++ b/internal/static/i18n/ko.yaml
@@ -537,6 +537,7 @@ Errors:
StateMissing: 요청에 상태 매개변수가 누락되었습니다
NotStarted: 의도가 시작되지 않았거나 이미 종료되었습니다
NotSucceeded: 의도가 성공하지 않았습니다
+ Expired: 의도의 유효 기간이 만료되었습니다
TokenCreationFailed: 토큰 생성 실패
InvalidToken: 의도 토큰이 유효하지 않습니다
OtherUser: 다른 사용자를 위한 의도입니다
diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml
index 898ed67360..7126925279 100644
--- a/internal/static/i18n/mk.yaml
+++ b/internal/static/i18n/mk.yaml
@@ -535,6 +535,7 @@ Errors:
StateMissing: Параметарот State недостасува во барањето
NotStarted: Намерата не е започната или веќе завршена
NotSucceeded: Намерата не е успешна
+ Expired: Намерата е истечена
TokenCreationFailed: Неуспешно креирање на токен
InvalidToken: Токенот за намера е невалиден
OtherUser: Намерата е за друг корисник
diff --git a/internal/static/i18n/nl.yaml b/internal/static/i18n/nl.yaml
index 882c58a4f2..a398e4b770 100644
--- a/internal/static/i18n/nl.yaml
+++ b/internal/static/i18n/nl.yaml
@@ -536,6 +536,7 @@ Errors:
StateMissing: Staat parameter ontbreekt in het verzoek
NotStarted: Intentie is niet gestart of was al beëindigd
NotSucceeded: Intentie is niet geslaagd
+ Expired: Intentie is verlopen
TokenCreationFailed: Token aanmaken mislukt
InvalidToken: Intentie Token is ongeldig
OtherUser: Intentie bedoeld voor een andere gebruiker
diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml
index 13125bc2a9..049a189930 100644
--- a/internal/static/i18n/pl.yaml
+++ b/internal/static/i18n/pl.yaml
@@ -536,6 +536,7 @@ Errors:
StateMissing: Brak parametru stanu w żądaniu
NotStarted: Intencja nie została rozpoczęta lub już się zakończyła
NotSucceeded: intencja nie powiodła się
+ Expired: Intencja wygasła
TokenCreationFailed: Tworzenie tokena nie powiodło się
InvalidToken: Token intencji jest nieprawidłowy
OtherUser: Intencja przeznaczona dla innego użytkownika
diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml
index 4ab3573c2b..09a5fc02c5 100644
--- a/internal/static/i18n/pt.yaml
+++ b/internal/static/i18n/pt.yaml
@@ -535,6 +535,7 @@ Errors:
StateMissing: O parâmetro de estado está faltando na solicitação
NotStarted: A intenção não foi iniciada ou já foi encerrada
NotSucceeded: A intenção não teve sucesso
+ Expired: A intenção expirou
TokenCreationFailed: Falha na criação do token
InvalidToken: O token da intenção é inválido
OtherUser: Intenção destinada a outro usuário
diff --git a/internal/static/i18n/ro.yaml b/internal/static/i18n/ro.yaml
index 48790da9e5..9010e57032 100644
--- a/internal/static/i18n/ro.yaml
+++ b/internal/static/i18n/ro.yaml
@@ -537,6 +537,7 @@ Errors:
StateMissing: Parametrul de stare lipsește în cerere
NotStarted: Intenția nu este pornită sau a fost deja terminată
NotSucceeded: Intenția nu a reușit
+ Expired: Intenția a expirat
TokenCreationFailed: Crearea token-ului a eșuat
InvalidToken: Token-ul intenției este invalid
OtherUser: Intenția este destinată altui utilizator
diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml
index 64a8ef8013..38b2847637 100644
--- a/internal/static/i18n/ru.yaml
+++ b/internal/static/i18n/ru.yaml
@@ -525,6 +525,7 @@ Errors:
StateMissing: В запросе отсутствует параметр State
NotStarted: Намерение не начато или уже прекращено
NotSucceeded: Намерение не увенчалось успехом
+ Epired: Намерение истекло
TokenCreationFailed: Не удалось создать токен
InvalidToken: Маркер намерения недействителен
OtherUser: Намерение, предназначенное для другого пользователя
diff --git a/internal/static/i18n/sv.yaml b/internal/static/i18n/sv.yaml
index 2c292976d3..ed4b863886 100644
--- a/internal/static/i18n/sv.yaml
+++ b/internal/static/i18n/sv.yaml
@@ -536,6 +536,7 @@ Errors:
StateMissing: State-parameter saknas i begäran
NotStarted: Avsikten har inte startat eller har redan avslutats
NotSucceeded: Avsikten har inte lyckats
+ Expired: Avsikten har gått ut
TokenCreationFailed: Token-skapande misslyckades
InvalidToken: Avsiktstoken är ogiltig
OtherUser: Avsikten är avsedd för en annan användare
diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml
index d4b36df7ff..03aa168a50 100644
--- a/internal/static/i18n/zh.yaml
+++ b/internal/static/i18n/zh.yaml
@@ -536,6 +536,7 @@ Errors:
StateMissing: 请求中缺少状态参数
NotStarted: 意图没有开始或已经结束
NotSucceeded: 意图不成功
+ Expired: 意图已过期
TokenCreationFailed: 令牌创建失败
InvalidToken: 意图令牌是无效的
OtherUser: 意图是为另一个用户准备的
From a626678004ee6a6ee02d814af2daebf673deed15 Mon Sep 17 00:00:00 2001
From: Silvan <27845747+adlerhurst@users.noreply.github.com>
Date: Tue, 6 May 2025 08:15:45 +0200
Subject: [PATCH 038/181] fix(setup): execute s54 (#9849)
# Which Problems Are Solved
Step 54 was not executed during setup.
# How the Problems Are Solved
Added the step to setup jobs
# Additional Changes
none
# Additional Context
- the step was added in https://github.com/zitadel/zitadel/pull/9837
- thanks to @zhirschtritt for raising this.
---
cmd/setup/config.go | 1 +
cmd/setup/setup.go | 2 ++
2 files changed, 3 insertions(+)
diff --git a/cmd/setup/config.go b/cmd/setup/config.go
index 4742b94c7b..127f9d7599 100644
--- a/cmd/setup/config.go
+++ b/cmd/setup/config.go
@@ -150,6 +150,7 @@ type Steps struct {
s51IDPTemplate6RootCA *IDPTemplate6RootCA
s52IDPTemplate6LDAP2 *IDPTemplate6LDAP2
s53InitPermittedOrgsFunction *InitPermittedOrgsFunction53
+ s54InstancePositionIndex *InstancePositionIndex
}
func MustNewSteps(v *viper.Viper) *Steps {
diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go
index f4df9fc71b..eead1980ed 100644
--- a/cmd/setup/setup.go
+++ b/cmd/setup/setup.go
@@ -212,6 +212,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
steps.s51IDPTemplate6RootCA = &IDPTemplate6RootCA{dbClient: dbClient}
steps.s52IDPTemplate6LDAP2 = &IDPTemplate6LDAP2{dbClient: dbClient}
steps.s53InitPermittedOrgsFunction = &InitPermittedOrgsFunction53{dbClient: dbClient}
+ steps.s54InstancePositionIndex = &InstancePositionIndex{dbClient: dbClient}
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil)
logging.OnError(err).Fatal("unable to start projections")
@@ -254,6 +255,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
steps.s51IDPTemplate6RootCA,
steps.s52IDPTemplate6LDAP2,
steps.s53InitPermittedOrgsFunction,
+ steps.s54InstancePositionIndex,
} {
setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed")
if setupErr != nil {
From 8cb1d24b36d4c7a588210c1a1649fa3b2a77dc7d Mon Sep 17 00:00:00 2001
From: Zach Hirschtritt
Date: Tue, 6 May 2025 02:38:19 -0400
Subject: [PATCH 039/181] fix: add user id index on sessions8 (#9834)
# Which Problems Are Solved
When a user changes their password, Zitadel needs to terminate all of
that user's active sessions. This query can take many seconds on
deployments with large session and user tables. This happens as part of
session projection handling, so doesn't directly impact user experience,
but potentially bogs down the projection handler which isn't great. In
the future, this index could be used to power a "see all of my current
sessions" feature in Zitadel.
# How the Problems Are Solved
Adds new index on `user_id` column on `projections.sessions8` table.
Alternatively, we can index on `(instance_id, user_id)` instead but
opted for keeping the index smaller as we already index on `instance_id`
separately.
# Additional Changes
None
# Additional Context
None
---------
Co-authored-by: Silvan <27845747+adlerhurst@users.noreply.github.com>
---
internal/query/projection/session.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/internal/query/projection/session.go b/internal/query/projection/session.go
index 196a352190..53eba54efb 100644
--- a/internal/query/projection/session.go
+++ b/internal/query/projection/session.go
@@ -87,6 +87,7 @@ func (*sessionProjection) Init() *old_handler.Check {
SessionColumnUserAgentFingerprintID+"_idx",
[]string{SessionColumnUserAgentFingerprintID},
)),
+ handler.WithIndex(handler.NewIndex(SessionColumnUserID+"_idx", []string{SessionColumnUserID})),
),
)
}
From 0d7d4e6af084153d9a778e1f03561ebd5e35b89e Mon Sep 17 00:00:00 2001
From: Stefan Benz <46600784+stebenz@users.noreply.github.com>
Date: Wed, 7 May 2025 10:14:01 +0200
Subject: [PATCH 040/181] docs: extend api design with additional information
and examples (#9856)
# Which Problems Are Solved
There were some misunderstandings on how different points would be
needed to be applied into existing API definitions.
# How the Problems Are Solved
- Added structure to the API design
- Added points to context information in requests and responses
- Added examples to responses with context information
- Corrected available pagination messages
- Added pagination and filter examples
# Additional Changes
None
# Additional Context
None
---
API_DESIGN.md | 114 +++++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 99 insertions(+), 15 deletions(-)
diff --git a/API_DESIGN.md b/API_DESIGN.md
index ea37df5a24..ac7c4a01e0 100644
--- a/API_DESIGN.md
+++ b/API_DESIGN.md
@@ -73,6 +73,8 @@ For example, use `organization_id` instead of **org_id** or **resource_owner** f
#### Resources and Fields
+##### Context information in Requests
+
When a context is required for creating a resource, the context is added as a field to the resource.
For example, when creating a new user, the organization's id is required. The `organization_id` is added as a field to the `CreateUserRequest`.
@@ -90,6 +92,65 @@ Only allow providing a context where it is required. The context MUST not be pro
For example, when retrieving or updating a user, the `organization_id` is not required, since the user can be determined by the user's id.
However, it is possible to provide the `organization_id` as a filter to retrieve a list of users of a specific organization.
+##### Context information in Responses
+
+When the action of creation, update or deletion of a resource was successful, the returned response has to include the time of the operation and the generated identifiers.
+This is achieved through the addition of a timestamp attribute with the operation as a prefix, and the generated information as separate attributes.
+
+```protobuf
+message SetExecutionResponse {
+ // The timestamp of the execution set.
+ google.protobuf.Timestamp set_date = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"2024-12-18T07:50:47.492Z\"";
+ }
+ ];
+}
+
+message CreateTargetResponse {
+ // The unique identifier of the newly created target.
+ string id = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"69629012906488334\"";
+ }
+ ];
+ // The timestamp of the target creation.
+ google.protobuf.Timestamp creation_date = 2 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"2024-12-18T07:50:47.492Z\"";
+ }
+ ];
+ // Key used to sign and check payload sent to the target.
+ string signing_key = 3 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"98KmsU67\""
+ }
+ ];
+}
+
+message UpdateProjectGrantResponse {
+ // The timestamp of the change of the project grant.
+ google.protobuf.Timestamp change_date = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"2025-01-23T10:34:18.051Z\"";
+ }
+ ];
+}
+
+message DeleteProjectGrantResponse {
+ // The timestamp of the deletion of the project grant.
+ // Note that the deletion date is only guaranteed to be set if the deletion was successful during the request.
+ // In case the deletion occurred in a previous request, the deletion date might be empty.
+ google.protobuf.Timestamp deletion_date = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"2025-01-23T10:34:18.051Z\"";
+ }
+ ];
+}
+```
+
+##### Global messages
+
Prevent the creation of global messages that are used in multiple resources unless they always follow the same pattern.
Use dedicated fields as described above or create a separate message for the specific context, that is only used in the boundary of the same resource.
For example, settings might be set as a default on the instance level, but might be overridden on the organization level.
@@ -99,6 +160,8 @@ The same applies to messages that are returned by multiple resources.
For example, information about the `User` might be different when managing the user resource itself than when it's returned
as part of an authorization or a manager role, where only limited information is needed.
+##### Re-using messages
+
Prevent reusing messages for the creation and the retrieval of a resource.
Returning messages might contain additional information that is not required or even not available for the creation of the resource.
What might sound obvious when designing the CreateUserRequest for example, where only an `organization_id` but not the
@@ -190,33 +253,54 @@ In case the permission cannot be checked by the API itself, but all requests nee
};
```
-## Pagination
+## Listing resources
The API uses pagination for listing resources. The client can specify a limit and an offset to retrieve a subset of the resources.
Additionally, the client can specify sorting options to sort the resources by a specific field.
-Most listing methods SHOULD provide use the `ListQuery` message to allow the client to specify the limit, offset, and sorting options.
-```protobuf
+### Pagination
-// ListQuery is a general query object for lists to allow pagination and sorting.
-message ListQuery {
- uint64 offset = 1;
- // limit is the maximum amount of objects returned. The default is set to 100
- // with a maximum of 1000 in the runtime configuration.
- // If the limit exceeds the maximum configured ZITADEL will throw an error.
- // If no limit is present the default is taken.
- uint32 limit = 2;
- // Asc is the sorting order. If true the list is sorted ascending, if false
- // the list is sorted descending. The default is descending.
- bool asc = 3;
+Most listing methods SHOULD use the `PaginationRequest` message to allow the client to specify the limit, offset, and sorting options.
+```protobuf
+message ListTargetsRequest {
+ // List limitations and ordering.
+ optional zitadel.filter.v2beta.PaginationRequest pagination = 1;
+ // The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent.
+ optional TargetFieldName sorting_column = 2 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ default: "\"TARGET_FIELD_NAME_CREATION_DATE\""
+ }
+ ];
+ // Define the criteria to query for.
+ repeated TargetSearchFilter filters = 3;
+ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = {
+ example: "{\"pagination\":{\"offset\":0,\"limit\":0,\"asc\":true},\"sortingColumn\":\"TARGET_FIELD_NAME_CREATION_DATE\",\"filters\":[{\"targetNameFilter\":{\"targetName\":\"ip_allow_list\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"inTargetIdsFilter\":{\"targetIds\":[\"69629023906488334\",\"69622366012355662\"]}}]}";
+ };
}
```
-On the corresponding responses the `ListDetails` can be used to return the total count of the resources
+
+On the corresponding responses the `PaginationResponse` can be used to return the total count of the resources
and allow the user to handle their offset and limit accordingly.
The API MUST enforce a reasonable maximum limit for the number of resources that can be retrieved and returned in a single request.
The default limit is set to 100 and the maximum limit is set to 1000. If the client requests a limit that exceeds the maximum limit, an error is returned.
+### Filter method
+
+All filters in List operations SHOULD provide a method if not already specified by the filters name.
+```protobuf
+message TargetNameFilter {
+ // Defines the name of the target to query for.
+ string target_name = 1 [
+ (validate.rules).string = {max_len: 200}
+ ];
+ // Defines which text comparison method used for the name query.
+ zitadel.filter.v2beta.TextFilterMethod method = 2 [
+ (validate.rules).enum.defined_only = true
+ ];
+}
+```
+
## Error Handling
The API returns machine-readable errors in the response body. This includes a status code, an error code and possibly
From 898366c537f59d2bfeff5dec740ee2ccba916ce7 Mon Sep 17 00:00:00 2001
From: Elio Bischof
Date: Wed, 7 May 2025 15:24:24 +0200
Subject: [PATCH 041/181] fix: allow user self deletion (#9828)
# Which Problems Are Solved
Currently, users can't delete themselves using the V2 RemoveUser API
because of the redunant API middleware permission check.
On main, using a machine user PAT to delete the same machine user:
```bash
grpcurl -plaintext -H "Authorization: Bearer ${ZITADEL_ACCESS_TOKEN}" -d '{"userId": "318838604669387137"}' localhost:8080 zitadel.user.v2.UserService.DeleteUser
ERROR:
Code: NotFound
Message: membership not found (AUTHZ-cdgFk)
Details:
1) {
"@type": "type.googleapis.com/zitadel.v1.ErrorDetail",
"id": "AUTHZ-cdgFk",
"message": "membership not found"
}
```
Same on this PRs branch:
```bash
grpcurl -plaintext -H "Authorization: Bearer ${ZITADEL_ACCESS_TOKEN}" -d '{"userId": "318838604669387137"}' localhost:8080 zitadel.user.v2.UserService.DeleteUser
{
"details": {
"sequence": "3",
"changeDate": "2025-05-06T13:44:54.349048Z",
"resourceOwner": "318838541083804033"
}
}
```
Repeated call
```bash
grpcurl -plaintext -H "Authorization: Bearer ${ZITADEL_ACCESS_TOKEN}" -d '{"userId": "318838604669387137"}' localhost:8080 zitadel.user.v2.UserService.DeleteUser
ERROR:
Code: Unauthenticated
Message: Errors.Token.Invalid (AUTH-7fs1e)
Details:
1) {
"@type": "type.googleapis.com/zitadel.v1.ErrorDetail",
"id": "AUTH-7fs1e",
"message": "Errors.Token.Invalid"
}
```
# How the Problems Are Solved
The middleware permission check is disabled and the
domain.PermissionCheck is used exclusively.
# Additional Changes
A new type command.PermissionCheck allows to optionally accept a
permission check for commands, so APIs with middleware permission checks
can omit redundant permission checks by passing nil while APIs without
middleware permission checks can pass one to the command.
# Additional Context
This is a subtask of #9763
---------
Co-authored-by: Livio Spring
---
.../user/v2/integration_test/user_test.go | 50 ++--
internal/command/permission_checks.go | 60 ++++
internal/command/permission_checks_test.go | 278 ++++++++++++++++++
internal/command/user_v2.go | 31 --
internal/command/user_v2_invite.go | 6 +-
internal/command/user_v2_invite_test.go | 16 +-
internal/command/user_v2_test.go | 93 ++++--
proto/zitadel/user/v2/user_service.proto | 2 +-
proto/zitadel/user/v2beta/user_service.proto | 2 +-
9 files changed, 459 insertions(+), 79 deletions(-)
create mode 100644 internal/command/permission_checks.go
create mode 100644 internal/command/permission_checks_test.go
diff --git a/internal/api/grpc/user/v2/integration_test/user_test.go b/internal/api/grpc/user/v2/integration_test/user_test.go
index 70e670bacc..eaf352c094 100644
--- a/internal/api/grpc/user/v2/integration_test/user_test.go
+++ b/internal/api/grpc/user/v2/integration_test/user_test.go
@@ -1756,9 +1756,8 @@ func TestServer_DeleteUser(t *testing.T) {
projectResp, err := Instance.CreateProject(CTX)
require.NoError(t, err)
type args struct {
- ctx context.Context
req *user.DeleteUserRequest
- prepare func(request *user.DeleteUserRequest) error
+ prepare func(*testing.T, *user.DeleteUserRequest) context.Context
}
tests := []struct {
name string
@@ -1769,23 +1768,21 @@ func TestServer_DeleteUser(t *testing.T) {
{
name: "remove, not existing",
args: args{
- CTX,
&user.DeleteUserRequest{
UserId: "notexisting",
},
- func(request *user.DeleteUserRequest) error { return nil },
+ func(*testing.T, *user.DeleteUserRequest) context.Context { return CTX },
},
wantErr: true,
},
{
name: "remove human, ok",
args: args{
- ctx: CTX,
req: &user.DeleteUserRequest{},
- prepare: func(request *user.DeleteUserRequest) error {
+ prepare: func(_ *testing.T, request *user.DeleteUserRequest) context.Context {
resp := Instance.CreateHumanUser(CTX)
request.UserId = resp.GetUserId()
- return err
+ return CTX
},
},
want: &user.DeleteUserResponse{
@@ -1798,12 +1795,11 @@ func TestServer_DeleteUser(t *testing.T) {
{
name: "remove machine, ok",
args: args{
- ctx: CTX,
req: &user.DeleteUserRequest{},
- prepare: func(request *user.DeleteUserRequest) error {
+ prepare: func(_ *testing.T, request *user.DeleteUserRequest) context.Context {
resp := Instance.CreateMachineUser(CTX)
request.UserId = resp.GetUserId()
- return err
+ return CTX
},
},
want: &user.DeleteUserResponse{
@@ -1816,15 +1812,37 @@ func TestServer_DeleteUser(t *testing.T) {
{
name: "remove dependencies, ok",
args: args{
- ctx: CTX,
req: &user.DeleteUserRequest{},
- prepare: func(request *user.DeleteUserRequest) error {
+ prepare: func(_ *testing.T, request *user.DeleteUserRequest) context.Context {
resp := Instance.CreateHumanUser(CTX)
request.UserId = resp.GetUserId()
Instance.CreateProjectUserGrant(t, CTX, projectResp.GetId(), request.UserId)
Instance.CreateProjectMembership(t, CTX, projectResp.GetId(), request.UserId)
Instance.CreateOrgMembership(t, CTX, request.UserId)
- return err
+ return CTX
+ },
+ },
+ want: &user.DeleteUserResponse{
+ Details: &object.Details{
+ ChangeDate: timestamppb.Now(),
+ ResourceOwner: Instance.DefaultOrg.Id,
+ },
+ },
+ },
+ {
+ name: "remove self, ok",
+ args: args{
+ req: &user.DeleteUserRequest{},
+ prepare: func(t *testing.T, request *user.DeleteUserRequest) context.Context {
+ removeUser, err := Instance.Client.Mgmt.AddMachineUser(CTX, &mgmt.AddMachineUserRequest{
+ UserName: gofakeit.Username(),
+ Name: gofakeit.Name(),
+ })
+ request.UserId = removeUser.UserId
+ require.NoError(t, err)
+ tokenResp, err := Instance.Client.Mgmt.AddPersonalAccessToken(CTX, &mgmt.AddPersonalAccessTokenRequest{UserId: removeUser.UserId})
+ require.NoError(t, err)
+ return integration.WithAuthorizationToken(UserCTX, tokenResp.Token)
},
},
want: &user.DeleteUserResponse{
@@ -1837,10 +1855,8 @@ func TestServer_DeleteUser(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- err := tt.args.prepare(tt.args.req)
- require.NoError(t, err)
-
- got, err := Client.DeleteUser(tt.args.ctx, tt.args.req)
+ ctx := tt.args.prepare(t, tt.args.req)
+ got, err := Client.DeleteUser(ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
diff --git a/internal/command/permission_checks.go b/internal/command/permission_checks.go
new file mode 100644
index 0000000000..bec2e9b7d4
--- /dev/null
+++ b/internal/command/permission_checks.go
@@ -0,0 +1,60 @@
+package command
+
+import (
+ "context"
+
+ "github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/v2/user"
+ "github.com/zitadel/zitadel/internal/zerrors"
+)
+
+type PermissionCheck func(resourceOwner, aggregateID string) error
+
+func (c *Commands) newPermissionCheck(ctx context.Context, permission string, aggregateType eventstore.AggregateType) PermissionCheck {
+ return func(resourceOwner, aggregateID string) error {
+ if aggregateID == "" {
+ return zerrors.ThrowInternal(nil, "COMMAND-ulBlS", "Errors.IDMissing")
+ }
+ // For example if a write model didn't query any events, the resource owner is probably empty.
+ // In this case, we have to query an event on the given aggregate to get the resource owner.
+ if resourceOwner == "" {
+ r := NewResourceOwnerModel(authz.GetInstance(ctx).InstanceID(), aggregateType, aggregateID)
+ err := c.eventstore.FilterToQueryReducer(ctx, r)
+ if err != nil {
+ return err
+ }
+ resourceOwner = r.resourceOwner
+ }
+ if resourceOwner == "" {
+ return zerrors.ThrowNotFound(nil, "COMMAND-4g3xq", "Errors.NotFound")
+ }
+ return c.checkPermission(ctx, permission, resourceOwner, aggregateID)
+ }
+}
+
+func (c *Commands) checkPermissionOnUser(ctx context.Context, permission string) PermissionCheck {
+ return func(resourceOwner, aggregateID string) error {
+ if aggregateID != "" && aggregateID == authz.GetCtxData(ctx).UserID {
+ return nil
+ }
+ return c.newPermissionCheck(ctx, permission, user.AggregateType)(resourceOwner, aggregateID)
+ }
+}
+
+func (c *Commands) NewPermissionCheckUserWrite(ctx context.Context) PermissionCheck {
+ return c.checkPermissionOnUser(ctx, domain.PermissionUserWrite)
+}
+
+func (c *Commands) checkPermissionDeleteUser(ctx context.Context, resourceOwner, userID string) error {
+ return c.checkPermissionOnUser(ctx, domain.PermissionUserDelete)(resourceOwner, userID)
+}
+
+func (c *Commands) checkPermissionUpdateUser(ctx context.Context, resourceOwner, userID string) error {
+ return c.NewPermissionCheckUserWrite(ctx)(resourceOwner, userID)
+}
+
+func (c *Commands) checkPermissionUpdateUserCredentials(ctx context.Context, resourceOwner, userID string) error {
+ return c.checkPermissionOnUser(ctx, domain.PermissionUserCredentialWrite)(resourceOwner, userID)
+}
diff --git a/internal/command/permission_checks_test.go b/internal/command/permission_checks_test.go
new file mode 100644
index 0000000000..5c36dc14f9
--- /dev/null
+++ b/internal/command/permission_checks_test.go
@@ -0,0 +1,278 @@
+package command
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/eventstore"
+ "github.com/zitadel/zitadel/internal/eventstore/repository"
+ "github.com/zitadel/zitadel/internal/zerrors"
+)
+
+func TestCommands_CheckPermission(t *testing.T) {
+ type fields struct {
+ eventstore func(*testing.T) *eventstore.Eventstore
+ domainPermissionCheck func(*testing.T) domain.PermissionCheck
+ }
+ type args struct {
+ ctx context.Context
+ permission string
+ aggregateType eventstore.AggregateType
+ resourceOwner, aggregateID string
+ }
+ type want struct {
+ err func(error) bool
+ }
+ ctx := context.Background()
+ filterErr := errors.New("filter error")
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ want want
+ }{
+ {
+ name: "resource owner is given, no query",
+ fields: fields{
+ domainPermissionCheck: mockDomainPermissionCheck(
+ ctx,
+ "permission",
+ "resourceOwner",
+ "aggregateID"),
+ },
+ args: args{
+ ctx: ctx,
+ permission: "permission",
+ resourceOwner: "resourceOwner",
+ aggregateID: "aggregateID",
+ },
+ },
+ {
+ name: "resource owner is empty, query for resource owner",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(&repository.Event{
+ AggregateID: "aggregateID",
+ ResourceOwner: sql.NullString{String: "resourceOwner"},
+ }),
+ ),
+ domainPermissionCheck: mockDomainPermissionCheck(ctx, "permission", "resourceOwner", "aggregateID"),
+ },
+ args: args{
+ ctx: ctx,
+ permission: "permission",
+ resourceOwner: "",
+ aggregateID: "aggregateID",
+ },
+ },
+ {
+ name: "resource owner is empty, query for resource owner, error",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilterError(filterErr),
+ ),
+ },
+ args: args{
+ ctx: ctx,
+ permission: "permission",
+ resourceOwner: "",
+ aggregateID: "aggregateID",
+ },
+ want: want{
+ err: func(err error) bool {
+ return errors.Is(err, filterErr)
+ },
+ },
+ },
+ {
+ name: "resource owner is empty, query for resource owner, no events",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(),
+ ),
+ },
+ args: args{
+ ctx: ctx,
+ permission: "permission",
+ resourceOwner: "",
+ aggregateID: "aggregateID",
+ },
+ want: want{
+ err: zerrors.IsNotFound,
+ },
+ },
+ {
+ name: "no aggregateID, internal error",
+ args: args{
+ ctx: ctx,
+ },
+ want: want{
+ err: zerrors.IsInternal,
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := &Commands{
+ checkPermission: func(ctx context.Context, permission, orgID, resourceID string) (err error) {
+ assert.Failf(t, "Domain permission check should not be called", "Called c.checkPermission(%v,%v,%v,%v)", ctx, permission, orgID, resourceID)
+ return nil
+ },
+ eventstore: expectEventstore()(t),
+ }
+ if tt.fields.domainPermissionCheck != nil {
+ c.checkPermission = tt.fields.domainPermissionCheck(t)
+ }
+ if tt.fields.eventstore != nil {
+ c.eventstore = tt.fields.eventstore(t)
+ }
+ err := c.newPermissionCheck(tt.args.ctx, tt.args.permission, tt.args.aggregateType)(tt.args.resourceOwner, tt.args.aggregateID)
+ if tt.want.err != nil {
+ assert.True(t, tt.want.err(err))
+ }
+ })
+ }
+}
+
+func TestCommands_CheckPermissionUserWrite(t *testing.T) {
+ type fields struct {
+ domainPermissionCheck func(*testing.T) domain.PermissionCheck
+ }
+ type args struct {
+ ctx context.Context
+ resourceOwner, aggregateID string
+ }
+ type want struct {
+ err func(error) bool
+ }
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ want want
+ }{
+ {
+ name: "self, no permission check",
+ args: args{
+ ctx: authz.SetCtxData(context.Background(), authz.CtxData{
+ UserID: "aggregateID",
+ }),
+ resourceOwner: "resourceOwner",
+ aggregateID: "aggregateID",
+ },
+ },
+ {
+ name: "not self, permission check",
+ fields: fields{
+ domainPermissionCheck: mockDomainPermissionCheck(
+ context.Background(),
+ "user.write",
+ "resourceOwner",
+ "foreignAggregateID"),
+ },
+ args: args{
+ ctx: context.Background(),
+ resourceOwner: "resourceOwner",
+ aggregateID: "foreignAggregateID",
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := &Commands{
+ checkPermission: func(ctx context.Context, permission, orgID, resourceID string) (err error) {
+ assert.Failf(t, "Domain permission check should not be called", "Called c.checkPermission(%v,%v,%v,%v)", ctx, permission, orgID, resourceID)
+ return nil
+ },
+ }
+ if tt.fields.domainPermissionCheck != nil {
+ c.checkPermission = tt.fields.domainPermissionCheck(t)
+ }
+ err := c.NewPermissionCheckUserWrite(tt.args.ctx)(tt.args.resourceOwner, tt.args.aggregateID)
+ if tt.want.err != nil {
+ assert.True(t, tt.want.err(err))
+ }
+ })
+ }
+}
+
+func TestCommands_CheckPermissionUserDelete(t *testing.T) {
+ type fields struct {
+ domainPermissionCheck func(*testing.T) domain.PermissionCheck
+ }
+ type args struct {
+ ctx context.Context
+ resourceOwner, aggregateID string
+ }
+ type want struct {
+ err func(error) bool
+ }
+ userCtx := authz.SetCtxData(context.Background(), authz.CtxData{
+ UserID: "aggregateID",
+ })
+ tests := []struct {
+ name string
+ fields fields
+ args args
+ want want
+ }{
+ {
+ name: "self, no permission check",
+ args: args{
+ ctx: userCtx,
+ resourceOwner: "resourceOwner",
+ aggregateID: "aggregateID",
+ },
+ },
+ {
+ name: "not self, permission check",
+ fields: fields{
+ domainPermissionCheck: mockDomainPermissionCheck(
+ context.Background(),
+ "user.delete",
+ "resourceOwner",
+ "foreignAggregateID"),
+ },
+ args: args{
+ ctx: context.Background(),
+ resourceOwner: "resourceOwner",
+ aggregateID: "foreignAggregateID",
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ c := &Commands{
+ checkPermission: func(ctx context.Context, permission, orgID, resourceID string) (err error) {
+ assert.Failf(t, "Domain permission check should not be called", "Called c.checkPermission(%v,%v,%v,%v)", ctx, permission, orgID, resourceID)
+ return nil
+ },
+ }
+ if tt.fields.domainPermissionCheck != nil {
+ c.checkPermission = tt.fields.domainPermissionCheck(t)
+ }
+ err := c.checkPermissionDeleteUser(tt.args.ctx, tt.args.resourceOwner, tt.args.aggregateID)
+ if tt.want.err != nil {
+ assert.True(t, tt.want.err(err))
+ }
+ })
+ }
+}
+
+func mockDomainPermissionCheck(expectCtx context.Context, expectPermission, expectResourceOwner, expectResourceID string) func(t *testing.T) domain.PermissionCheck {
+ return func(t *testing.T) domain.PermissionCheck {
+ return func(ctx context.Context, permission, orgID, resourceID string) (err error) {
+ assert.Equal(t, expectCtx, ctx)
+ assert.Equal(t, expectPermission, permission)
+ assert.Equal(t, expectResourceOwner, orgID)
+ assert.Equal(t, expectResourceID, resourceID)
+ return nil
+ }
+ }
+}
diff --git a/internal/command/user_v2.go b/internal/command/user_v2.go
index 00ca85aaf4..5f8e8d6ff5 100644
--- a/internal/command/user_v2.go
+++ b/internal/command/user_v2.go
@@ -5,7 +5,6 @@ import (
"github.com/zitadel/logging"
- "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/user"
@@ -117,36 +116,6 @@ func (c *Commands) ReactivateUserV2(ctx context.Context, userID string) (*domain
return writeModelToObjectDetails(&existingHuman.WriteModel), nil
}
-func (c *Commands) checkPermissionUpdateUser(ctx context.Context, resourceOwner, userID string) error {
- if userID != "" && userID == authz.GetCtxData(ctx).UserID {
- return nil
- }
- if err := c.checkPermission(ctx, domain.PermissionUserWrite, resourceOwner, userID); err != nil {
- return err
- }
- return nil
-}
-
-func (c *Commands) checkPermissionUpdateUserCredentials(ctx context.Context, resourceOwner, userID string) error {
- if userID != "" && userID == authz.GetCtxData(ctx).UserID {
- return nil
- }
- if err := c.checkPermission(ctx, domain.PermissionUserCredentialWrite, resourceOwner, userID); err != nil {
- return err
- }
- return nil
-}
-
-func (c *Commands) checkPermissionDeleteUser(ctx context.Context, resourceOwner, userID string) error {
- if userID != "" && userID == authz.GetCtxData(ctx).UserID {
- return nil
- }
- if err := c.checkPermission(ctx, domain.PermissionUserDelete, resourceOwner, userID); err != nil {
- return err
- }
- return nil
-}
-
func (c *Commands) userStateWriteModel(ctx context.Context, userID string) (writeModel *UserV2WriteModel, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
diff --git a/internal/command/user_v2_invite.go b/internal/command/user_v2_invite.go
index 1325d2e0c9..7760107146 100644
--- a/internal/command/user_v2_invite.go
+++ b/internal/command/user_v2_invite.go
@@ -73,12 +73,12 @@ func (c *Commands) ResendInviteCode(ctx context.Context, userID, resourceOwner,
if err != nil {
return nil, err
}
- if err := c.checkPermissionUpdateUser(ctx, existingCode.ResourceOwner, userID); err != nil {
- return nil, err
- }
if !existingCode.UserState.Exists() {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-H3b2a", "Errors.User.NotFound")
}
+ if err := c.checkPermissionUpdateUser(ctx, existingCode.ResourceOwner, userID); err != nil {
+ return nil, err
+ }
if !existingCode.CreationAllowed() {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Gg42s", "Errors.User.AlreadyInitialised")
}
diff --git a/internal/command/user_v2_invite_test.go b/internal/command/user_v2_invite_test.go
index efb57d86ad..817987e7e4 100644
--- a/internal/command/user_v2_invite_test.go
+++ b/internal/command/user_v2_invite_test.go
@@ -323,7 +323,21 @@ func TestCommands_ResendInviteCode(t *testing.T) {
"missing permission",
fields{
eventstore: expectEventstore(
- expectFilter(),
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanAddedEvent(context.Background(),
+ &user.NewAggregate("userID", "org1").Aggregate,
+ "username", "firstName",
+ "lastName",
+ "nickName",
+ "displayName",
+ language.Afrikaans,
+ domain.GenderUnspecified,
+ "email",
+ false,
+ ),
+ ),
+ ),
),
checkPermission: newMockPermissionCheckNotAllowed(),
},
diff --git a/internal/command/user_v2_test.go b/internal/command/user_v2_test.go
index 3eb9ecd6f7..685ad95253 100644
--- a/internal/command/user_v2_test.go
+++ b/internal/command/user_v2_test.go
@@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/assert"
"golang.org/x/text/language"
+ "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/org"
@@ -1081,13 +1082,14 @@ func TestCommandSide_ReactivateUserV2(t *testing.T) {
}
func TestCommandSide_RemoveUserV2(t *testing.T) {
+ ctxUserID := "ctxUserID"
+ ctx := authz.SetCtxData(context.Background(), authz.CtxData{UserID: ctxUserID})
type fields struct {
eventstore func(*testing.T) *eventstore.Eventstore
checkPermission domain.PermissionCheck
}
type (
args struct {
- ctx context.Context
userID string
cascadingMemberships []*CascadingMembership
grantIDs []string
@@ -1110,7 +1112,6 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
- ctx: context.Background(),
userID: "",
},
res: res{
@@ -1128,7 +1129,6 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
- ctx: context.Background(),
userID: "user1",
},
res: res{
@@ -1143,7 +1143,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
- user.NewHumanAddedEvent(context.Background(),
+ user.NewHumanAddedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
@@ -1157,7 +1157,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
),
),
eventFromEventPusher(
- user.NewUserRemovedEvent(context.Background(),
+ user.NewUserRemovedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
"username",
nil,
@@ -1169,7 +1169,6 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
- ctx: context.Background(),
userID: "user1",
},
res: res{
@@ -1184,7 +1183,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
- user.NewHumanAddedEvent(context.Background(),
+ user.NewHumanAddedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
@@ -1200,7 +1199,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
),
expectFilter(
eventFromEventPusher(
- org.NewDomainPolicyAddedEvent(context.Background(),
+ org.NewDomainPolicyAddedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
true,
true,
@@ -1209,7 +1208,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
),
),
expectPush(
- user.NewUserRemovedEvent(context.Background(),
+ user.NewUserRemovedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
"username",
nil,
@@ -1220,7 +1219,6 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
- ctx: context.Background(),
userID: "user1",
},
res: res{
@@ -1235,7 +1233,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
- user.NewHumanAddedEvent(context.Background(),
+ user.NewHumanAddedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
@@ -1249,7 +1247,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
),
),
eventFromEventPusher(
- user.NewHumanInitializedCheckSucceededEvent(context.Background(),
+ user.NewHumanInitializedCheckSucceededEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
),
),
@@ -1258,13 +1256,10 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
checkPermission: newMockPermissionCheckNotAllowed(),
},
args: args{
- ctx: context.Background(),
userID: "user1",
},
res: res{
- err: func(err error) bool {
- return errors.Is(err, zerrors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"))
- },
+ err: zerrors.IsPermissionDenied,
},
},
{
@@ -1273,7 +1268,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
- user.NewMachineAddedEvent(context.Background(),
+ user.NewMachineAddedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"name",
@@ -1283,7 +1278,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
),
),
eventFromEventPusher(
- user.NewUserRemovedEvent(context.Background(),
+ user.NewUserRemovedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
"username",
nil,
@@ -1292,10 +1287,8 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
),
),
),
- checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
- ctx: context.Background(),
userID: "user1",
},
res: res{
@@ -1310,7 +1303,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
- user.NewMachineAddedEvent(context.Background(),
+ user.NewMachineAddedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"name",
@@ -1322,7 +1315,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
),
expectFilter(
eventFromEventPusher(
- org.NewDomainPolicyAddedEvent(context.Background(),
+ org.NewDomainPolicyAddedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
true,
true,
@@ -1331,7 +1324,7 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
),
),
expectPush(
- user.NewUserRemovedEvent(context.Background(),
+ user.NewUserRemovedEvent(ctx,
&user.NewAggregate("user1", "org1").Aggregate,
"username",
nil,
@@ -1342,7 +1335,6 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
checkPermission: newMockPermissionCheckAllowed(),
},
args: args{
- ctx: context.Background(),
userID: "user1",
},
res: res{
@@ -1351,6 +1343,56 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
},
},
},
+ {
+ name: "remove self, ok",
+ fields: fields{
+ eventstore: expectEventstore(
+ expectFilter(
+ eventFromEventPusher(
+ user.NewHumanAddedEvent(ctx,
+ &user.NewAggregate(ctxUserID, "org1").Aggregate,
+ "username",
+ "firstname",
+ "lastname",
+ "nickname",
+ "displayname",
+ language.German,
+ domain.GenderUnspecified,
+ "email@test.ch",
+ true,
+ ),
+ ),
+ ),
+ expectFilter(
+ eventFromEventPusher(
+ org.NewDomainPolicyAddedEvent(ctx,
+ &user.NewAggregate(ctxUserID, "org1").Aggregate,
+ true,
+ true,
+ true,
+ ),
+ ),
+ ),
+ expectPush(
+ user.NewUserRemovedEvent(ctx,
+ &user.NewAggregate(ctxUserID, "org1").Aggregate,
+ "username",
+ nil,
+ true,
+ ),
+ ),
+ ),
+ checkPermission: newMockPermissionCheckNotAllowed(),
+ },
+ args: args{
+ userID: ctxUserID,
+ },
+ res: res{
+ want: &domain.ObjectDetails{
+ ResourceOwner: "org1",
+ },
+ },
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -1358,7 +1400,8 @@ func TestCommandSide_RemoveUserV2(t *testing.T) {
eventstore: tt.fields.eventstore(t),
checkPermission: tt.fields.checkPermission,
}
- got, err := r.RemoveUserV2(tt.args.ctx, tt.args.userID, "", tt.args.cascadingMemberships, tt.args.grantIDs...)
+
+ got, err := r.RemoveUserV2(ctx, tt.args.userID, "", tt.args.cascadingMemberships, tt.args.grantIDs...)
if tt.res.err == nil {
assert.NoError(t, err)
}
diff --git a/proto/zitadel/user/v2/user_service.proto b/proto/zitadel/user/v2/user_service.proto
index 00cb352f70..15bc2d7775 100644
--- a/proto/zitadel/user/v2/user_service.proto
+++ b/proto/zitadel/user/v2/user_service.proto
@@ -539,7 +539,7 @@ service UserService {
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
- permission: "user.delete"
+ permission: "authenticated"
}
};
diff --git a/proto/zitadel/user/v2beta/user_service.proto b/proto/zitadel/user/v2beta/user_service.proto
index 03bc36220e..f877252f51 100644
--- a/proto/zitadel/user/v2beta/user_service.proto
+++ b/proto/zitadel/user/v2beta/user_service.proto
@@ -563,7 +563,7 @@ service UserService {
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
- permission: "user.delete"
+ permission: "authenticated"
}
};
From c6aa6385b640f64d55e9ac273de4add22c99e2ba Mon Sep 17 00:00:00 2001
From: Stefan Benz <46600784+stebenz@users.noreply.github.com>
Date: Wed, 7 May 2025 15:59:02 +0200
Subject: [PATCH 042/181] docs: add invalid information to member requests
(#9858)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Which Problems Are Solved
Misleading information on member endpoint requests.
# How the Problems Are Solved
Add comment to member endpoint requests that the request is invalid if
no roles are provided.
# Additional Changes
None
# Additional Context
Closes #9415
Co-authored-by: Fabienne Bühler
---
proto/zitadel/management.proto | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto
index e69e331f87..84c7823009 100644
--- a/proto/zitadel/management.proto
+++ b/proto/zitadel/management.proto
@@ -9209,7 +9209,7 @@ message AddOrgMemberRequest {
repeated string roles = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"ORG_OWNER\"]";
- description: "If no roles are provided the user won't have any rights"
+ description: "If no roles are provided the user won't have any rights, so the member definition will be regarded as invalid."
}
];
}
@@ -9222,7 +9222,7 @@ message UpdateOrgMemberRequest {
repeated string roles = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"IAM_OWNER\"]";
- description: "If no roles are provided the user won't have any rights"
+ description: "If no roles are provided the user won't have any rights, so the member definition will be regarded as invalid."
}
];
}
@@ -9643,7 +9643,7 @@ message AddProjectMemberRequest {
repeated string roles = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"PROJECT_OWNER\"]";
- description: "If no roles are provided the user won't have any rights"
+ description: "If no roles are provided the user won't have any rights, so the member definition will be regarded as invalid."
}
];
}
@@ -9658,7 +9658,7 @@ message UpdateProjectMemberRequest {
repeated string roles = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"PROJECT_OWNER\"]";
- description: "If no roles are provided the user won't have any rights"
+ description: "If no roles are provided the user won't have any rights, so the member definition will be regarded as invalid."
}
];
}
@@ -10313,7 +10313,7 @@ message AddProjectGrantMemberRequest {
repeated string roles = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"PROJECT_GRANT_OWNER\"]";
- description: "If no roles are provided the user won't have any rights"
+ description: "If no roles are provided the user won't have any rights, so the member definition will be regarded as invalid."
}
];
}
@@ -10337,7 +10337,7 @@ message UpdateProjectGrantMemberRequest {
repeated string roles = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"PROJECT_GRANT_OWNER\"]";
- description: "If no roles are provided the user won't have any rights"
+ description: "If no roles are provided the user won't have any rights, so the member definition will be regarded as invalid."
}
];
}
From 21167a4bba8b74720422f2b09ff6753ddb24b67d Mon Sep 17 00:00:00 2001
From: Stefan Benz <46600784+stebenz@users.noreply.github.com>
Date: Wed, 7 May 2025 16:26:53 +0200
Subject: [PATCH 043/181] fix: add current state for execution handler into
setup (#9863)
# Which Problems Are Solved
The execution handler projection handles all events to check if an
execution has to be provided to the worker to execute.
In this logic all events would be processed from the beginning which is
not necessary.
# How the Problems Are Solved
Add the current state to the execution handler projection, to avoid
processing all existing events.
# Additional Changes
Add custom configuration to the default, so that the transactions are
limited to some events.
# Additional Context
None
---
cmd/defaults.yaml | 3 ++-
cmd/setup/38.go | 1 -
cmd/setup/55.go | 27 +++++++++++++++++++++++++++
cmd/setup/55.sql | 22 ++++++++++++++++++++++
cmd/setup/config.go | 1 +
cmd/setup/setup.go | 4 +++-
cmd/start/start.go | 2 +-
7 files changed, 56 insertions(+), 4 deletions(-)
create mode 100644 cmd/setup/55.go
create mode 100644 cmd/setup/55.sql
diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml
index 0d71b4d817..c13d5337b1 100644
--- a/cmd/defaults.yaml
+++ b/cmd/defaults.yaml
@@ -387,7 +387,8 @@ Projections:
org_domain_verified_fields:
TransactionDuration: 0s
BulkLimit: 2000
-
+ execution_handler:
+ BulkLimit: 10
# The Notifications projection is used for preparing the messages (emails and SMS) to be sent to users
Notifications:
# As notification projections don't result in database statements, retries don't have an effect
diff --git a/cmd/setup/38.go b/cmd/setup/38.go
index 0a102c9d12..810510bfdd 100644
--- a/cmd/setup/38.go
+++ b/cmd/setup/38.go
@@ -15,7 +15,6 @@ var (
type BackChannelLogoutNotificationStart struct {
dbClient *database.DB
- esClient *eventstore.Eventstore
}
func (mig *BackChannelLogoutNotificationStart) Execute(ctx context.Context, e eventstore.Event) error {
diff --git a/cmd/setup/55.go b/cmd/setup/55.go
new file mode 100644
index 0000000000..19083515e5
--- /dev/null
+++ b/cmd/setup/55.go
@@ -0,0 +1,27 @@
+package setup
+
+import (
+ "context"
+ _ "embed"
+
+ "github.com/zitadel/zitadel/internal/database"
+ "github.com/zitadel/zitadel/internal/eventstore"
+)
+
+var (
+ //go:embed 55.sql
+ executionHandlerCurrentState string
+)
+
+type ExecutionHandlerStart struct {
+ dbClient *database.DB
+}
+
+func (mig *ExecutionHandlerStart) Execute(ctx context.Context, e eventstore.Event) error {
+ _, err := mig.dbClient.ExecContext(ctx, executionHandlerCurrentState, e.Sequence(), e.CreatedAt(), e.Position())
+ return err
+}
+
+func (mig *ExecutionHandlerStart) String() string {
+ return "55_execution_handler_start"
+}
diff --git a/cmd/setup/55.sql b/cmd/setup/55.sql
new file mode 100644
index 0000000000..60c45d5f94
--- /dev/null
+++ b/cmd/setup/55.sql
@@ -0,0 +1,22 @@
+INSERT INTO projections.current_states AS cs ( instance_id
+ , projection_name
+ , last_updated
+ , sequence
+ , event_date
+ , position
+ , filter_offset)
+SELECT instance_id
+ , 'projections.execution_handler'
+ , now()
+ , $1
+ , $2
+ , $3
+ , 0
+FROM eventstore.events2 AS e
+WHERE aggregate_type = 'instance'
+ AND event_type = 'instance.added'
+ON CONFLICT (instance_id, projection_name) DO UPDATE SET last_updated = EXCLUDED.last_updated,
+ sequence = EXCLUDED.sequence,
+ event_date = EXCLUDED.event_date,
+ position = EXCLUDED.position,
+ filter_offset = EXCLUDED.filter_offset;
\ No newline at end of file
diff --git a/cmd/setup/config.go b/cmd/setup/config.go
index 127f9d7599..5e5c842b14 100644
--- a/cmd/setup/config.go
+++ b/cmd/setup/config.go
@@ -151,6 +151,7 @@ type Steps struct {
s52IDPTemplate6LDAP2 *IDPTemplate6LDAP2
s53InitPermittedOrgsFunction *InitPermittedOrgsFunction53
s54InstancePositionIndex *InstancePositionIndex
+ s55ExecutionHandlerStart *ExecutionHandlerStart
}
func MustNewSteps(v *viper.Viper) *Steps {
diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go
index eead1980ed..58bc89d2e4 100644
--- a/cmd/setup/setup.go
+++ b/cmd/setup/setup.go
@@ -198,7 +198,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
steps.s35AddPositionToIndexEsWm = &AddPositionToIndexEsWm{dbClient: dbClient}
steps.s36FillV2Milestones = &FillV3Milestones{dbClient: dbClient, eventstore: eventstoreClient}
steps.s37Apps7OIDConfigsBackChannelLogoutURI = &Apps7OIDConfigsBackChannelLogoutURI{dbClient: dbClient}
- steps.s38BackChannelLogoutNotificationStart = &BackChannelLogoutNotificationStart{dbClient: dbClient, esClient: eventstoreClient}
+ steps.s38BackChannelLogoutNotificationStart = &BackChannelLogoutNotificationStart{dbClient: dbClient}
steps.s40InitPushFunc = &InitPushFunc{dbClient: dbClient}
steps.s42Apps7OIDCConfigsLoginVersion = &Apps7OIDCConfigsLoginVersion{dbClient: dbClient}
steps.s43CreateFieldsDomainIndex = &CreateFieldsDomainIndex{dbClient: dbClient}
@@ -213,6 +213,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
steps.s52IDPTemplate6LDAP2 = &IDPTemplate6LDAP2{dbClient: dbClient}
steps.s53InitPermittedOrgsFunction = &InitPermittedOrgsFunction53{dbClient: dbClient}
steps.s54InstancePositionIndex = &InstancePositionIndex{dbClient: dbClient}
+ steps.s55ExecutionHandlerStart = &ExecutionHandlerStart{dbClient: dbClient}
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil)
logging.OnError(err).Fatal("unable to start projections")
@@ -256,6 +257,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
steps.s52IDPTemplate6LDAP2,
steps.s53InitPermittedOrgsFunction,
steps.s54InstancePositionIndex,
+ steps.s55ExecutionHandlerStart,
} {
setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed")
if setupErr != nil {
diff --git a/cmd/start/start.go b/cmd/start/start.go
index e3d84625b4..52d9c6fba8 100644
--- a/cmd/start/start.go
+++ b/cmd/start/start.go
@@ -304,7 +304,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
execution.Register(
ctx,
- config.Projections.Customizations["executions"],
+ config.Projections.Customizations["execution_handler"],
config.Executions,
queries,
eventstoreClient.EventTypes(),
From 577bf9c710490ee34d54f7ffc9e3ae79ae556899 Mon Sep 17 00:00:00 2001
From: Maximilian
Date: Wed, 7 May 2025 17:58:21 +0200
Subject: [PATCH 044/181] docs(legal): Update to DPA and privacy policy
documents (May 2025) (#9566)
We are bringing our DPA and privacy policy document in line with our
changes to the corporate structure, changes to subprocessors, and new
cookie technologies.
This PR replaces #3055 which included more changes to terms of service.
The changes to terms of service will follow in a second step.
---------
Co-authored-by: Florian Forster
---
docs/docs/legal/data-processing-agreement.mdx | 274 ++++++++++++++----
docs/docs/legal/policies/privacy-policy.mdx | 231 ++++++++++-----
docs/src/components/pii_table.jsx | 106 +++++++
docs/src/components/subprocessors.jsx | 162 -----------
4 files changed, 490 insertions(+), 283 deletions(-)
create mode 100644 docs/src/components/pii_table.jsx
delete mode 100644 docs/src/components/subprocessors.jsx
diff --git a/docs/docs/legal/data-processing-agreement.mdx b/docs/docs/legal/data-processing-agreement.mdx
index 63a393605f..78f14aa730 100644
--- a/docs/docs/legal/data-processing-agreement.mdx
+++ b/docs/docs/legal/data-processing-agreement.mdx
@@ -1,113 +1,277 @@
---
title: Data Processing Agreement
custom_edit_url: null
-custom:
- created_at: 2022-07-15
- updated_at: 2023-11-16
---
-import PiidTable from './_piid-table.mdx';
-Last updated on November 15, 2023
+Last updated on May 8, 2025
-Within the scope of the [**Framework Agreement**](terms-of-service), the **Processor** (CAOS Ltd., also **ZITADEL**) processes **Personal Data** on behalf of the **Customer** (Responsible Party), collectively the **"Parties"**.
+This Data Protection Agreement and its annexes (“**DPA**”) are part of the [Framework Agreement](./terms-of-service) between Zitadel, Inc. and it's affiliates ("**Zitadel**") and the Customer in respect of the provision of certain services, including any applicable statement of work, booking, purchase order (PO) or any agreed upon instructions (the "**Agreement**") and applies where, and to the extent that, Zitadel processes Personal Data as a Processor on behalf of the Customer under the Framework Agreement (each a “**Party**” and together the “**Parties**”).
-This Annex to the Agreement governs the Parties' data protection obligations in addition to the provisions of the Agreement.
+All capitalized terms not defined in this DPA will have the meanings set forth in the Agreement.
+Any privacy or data protection related clauses or agreement previously entered into by Zitadel and the Customer, with regards to the subject matter of this DPA, will be superseded and replaced by this DPA.
+No one other than a Party to this DPA, their successors and permitted assignees will have any right to enforce any of its terms.
-## Subject matter, duration, nature and purpose of the processing as well as the type of personal data and categories of data subjects
+This DPA shall become legally binding upon Customer entering into the Agreement.
-This annex reflects the commitment of both parties to abide by the applicable data protection laws for the processing of Personal Data for the purpose of Processor's execution of the Framework Agreement.
+## Definitions
-The duration of the Processing shall correspond to the duration of the Agreement, unless otherwise provided for in this Annex or unless individual provisions obviously result in obligations going beyond this.
+"**Applicable Data Protection Law**" means all worldwide data protection and privacy laws and regulations applicable to the Personal Data, including, where applicable, EU/UK Data Protection Law and US Data Protection Laws (in each case, as amended, adopted, or superseded from time to time).
-In particular, the following Personal Data are part of the processing:
-
+“**Controller**,” “**collecting**,” “**processor**,” and “**processing**,” shall have the meanings given to them under Applicable Data Protection Law.
-## Scope and responsibility
+“**Business**,” “**service provider**,” “**contractor**,” “**selling**,” “**sharing**” and “**third party**” shall have the meanings given to them under applicable US Data Protection Laws.
-Under this Agreement, the Processor shall process Personal Data on behalf of the Customer.
+"**Customer Data**" means information, data and other content, in any form or medium, that is submitted, posted or otherwise transmitted by or on behalf of the Customer through the Zitadel Cloud or Services. For the avoidance of doubt, Customer Data includes Customer Personal Data.
-This Annex applies to all processing of Customer's data (including data of the users of Customer's organization) with reference to persons ("**Personal Data**") which is related to the Agreement and which is carried out by the Processor, its employees or agents.
+“**Customer Personal Data**” means, in any form or medium, all Personal Data that is processed by Zitadel or its sub-processors on behalf of Customer in connection with the Agreement.
-The Customer shall be responsible for compliance with the statutory provisions of the data protection laws, in particular for the lawfulness of the transfer of data to the Processor as well as for the lawfulness of the data processing.
+“**EU/UK Data Protection Law**” means: (i) Regulation 2016/679 of the European Parliament and of the Council on the protection of natural persons with regard to the processing of Personal Data and on the free movement of such data, also known as the General Data Protection Regulation (“**GDPR**”); (ii) the GDPR as saved into United Kingdom law by virtue of section 3 of the United Kingdom’s European Union (Withdrawal) Act 2018 (“**UK GDPR**”); (iii) the EU e-Privacy Directive (Directive 2002/58/EC); (iv) the Swiss Federal Act on Data Protection of 2020 and its Ordinance (“**Swiss FADP**”) and (v) any and all applicable national data protection laws and regulatory requirements made under, pursuant to or that apply in conjunction with any of (i), (ii) or (iii); in each case as may be amended or superseded from time to time.
-The Processor is responsible for taking appropriate technical and organizational protection measures so that its processing complies with the legal requirements and ensures the protection of the rights of the Data Subjects.
+“**Personal Data**” shall have the meaning given to it, or to the terms “personally identifiable information” and “personal information” under applicable Data Protection Law, but shall include, at a minimum, any information related to an identified or identifiable natural person.
+
+“**Restricted Transfer**” means: (i) where the GDPR applies, a transfer of Personal Data from the EEA to a country outside of the EEA which is not subject to an adequacy determination by the European Commission; and (ii) where UK-GDPR applies, a transfer of Personal Data from the United Kingdom to any other country which is not subject to adequacy regulations pursuant to Section 17A of the United Kingdom Data Protection Act 2018, in each case whether such transfer is direct or via onward transfer.
+
+“**Security Incident**” means any unauthorized or unlawful breach of security leading to, or reasonably believed to have led to, the accidental or unlawful destruction, loss, or alteration of, or unauthorized disclosure or access to, Personal Data transmitted, stored or otherwise processed by Zitadel under or in connection with the Agreement.
+
+“**Standard Contractual Clauses**” or “**SCCs**” means the contractual clauses annexed to the European Commission’s Implementing Decision 2021/914 of 4 June 2021 on standard contractual clauses for the transfer of personal data to third countries pursuant to Regulation (EU) 2016/679 of the European Parliament and of the Council.
+
+“**sub-processor**” means any third-party processor engaged by Zitadel to process Customer Data (but shall not include Zitadel employees, contractors or consultants).
+
+“**UK Addendum**” means the International Data Transfer Addendum (version B1.0) issued by the Information Commissioner’s Office under S119(A) of the UK Data Protection Act 2018, as updated or amended from time to time.
+
+“**US Data Protection Laws**” means any relevant U.S. federal and state privacy laws (and any implementing regulations and amendment thereto) effective as of the date of this DPA and that applies to the processing of Customer Personal Data under the Agreement, which may include, depending on the circumstances and without limitation, (i) the California Consumer Privacy Act (Cal. Civ. Code §§ 1798.100 et seq.), as amended by the California Privacy Rights Act of 2020 along with its implementing regulations (“**CCPA**”), (ii) the Colorado Privacy Act (Colo. Rev. Stat. §§ 6-1-1301 et seq.) (CPA), (iii) Connecticut’s Data Privacy Act (CTDPA), (iv) the Utah Consumer Privacy Act (Utah Code Ann. §§ 13-61-101 et seq.) (UCPA) and (v) the Virginia Consumer Data Protection Act VA Code Ann. §§ 59.1-575 et seq. (VCDPA).
+
+## Processing of Personal Data
+
+This DPA applies where and only to the extent that Zitadel processes Customer Personal Data in connection with the provision of the Services under the Agreement involving the processing of Personal Data protected by Applicable Data Protection Law.
+This DPA reflects the commitment of both Parties to abide by Applicable Data Protection Law for the processing of Personal Data by Zitadel as a processor for the purpose of the Zitadel's provision of the Services and its execution of the Agreement.
+
+This DPA will become effective on the date the Agreement enters into effect and will remain in force for the term of the Agreement, unless otherwise provided for in this DPA or unless individual provisions obviously result in obligations going beyond this.
+For the avoidance of doubt, the terms of the Framework Agreement will continue in full force and effect; however, to the extent any term in any Agreement regarding either Party’s obligations with respect to Customer Data is less restrictive than or is inconsistent with this DPA, the terms of this DPA shall supersede and control.
+
+The Parties acknowledge that the following Customer Data will be processed as part of the Services:
+
+import { PiiTable } from "../../src/components/pii_table";
+
+
+
+## Scope
+
+Under this Agreement, Zitadel shall process Customer Personal Data to perform its obligations under the Agreement and and strictly in accordance with the documented instructions of Customer (the “**Permitted Purpose**”), except where otherwise required by law(s) that are not incompatible with Applicable Data Protection Law.
+
+The Parties acknowledge and agree that for the purposes of this DPA, the Customer is the controller and appoints Zitadel as a processor to process the Customer Personal Data.
+To the extent that the Parties are subject to the California Consumer Privacy Act (CCPA), the Customer is the business whereas Zitadel is a service provider to the Customer.
+Each Party shall comply with the obligations that apply to it under Applicable Data Protection Law.
+
+Each Party shall comply with its own obligations under Applicable Data Protection Law in respect of any Customer Personal Data processed under the Agreement.
+
+## Customer's Responsibilities
+
+The Customer’s instructions to Zitadel shall comply with Applicable Data Protection Law.
+The Customer will have sole responsibility for the accuracy, quality and legality of the Customer Data, the means by which the Customer acquired the Customer Data, and the Customer's permissions to process the Customer Data pursuant to this DPA.
+
+As required under Applicable Data Protection Law, the Customer will provide all necessary notices to data subjects and secure the applicable lawful grounds for processing Data under the DPA, including where applicable, all necessary permissions and consents from them. To the extent required under Applicable Data Protection Law, the Customer will receive and document the appropriate consent from the data subject(s).
+
+The Customer represents and warrants that (i) it complies with Applicable Data Protection Law as relevant to the lawful processing by Zitadel of Customer Personal Data for the purposes contemplated by this DPA and the Agreement; and (ii) to the knowledge of the Customer, the processing of Customer Personal Data by Zitadel in accordance with the Customer’s instructions will not cause Zitadel to be in breach of any Applicable Data Protection Law.
+
+The Customer shall not disclose any special categories of Personal Data or sensitive personal information (as these terms are defined under Applicable Data Protection Law) to Zitadel for processing.
## Obligations of the processor
-### Bound by directions
+### Bound by the Customer's directions and instructions
-The Processor processes personal data in accordance with its privacy policy (cf. [Privacy Policy](/legal/policies/privacy-policy)) and on the documented directions of the Customer. The initial direction result from the Agreement. Subsequent instructions shall be given either in writing, whereby e-mail shall suffice, or orally with immediate written confirmation.
+Customer hereby instructs Zitadel to process Customer Data for the Permitted Purpose.
-If the Processor is of the opinion that a direction of the Customer violates the Agreement, the GDPR or other data protection provisions of the EU, EU Member States or Switzerland, it shall inform the Customer thereof and shall be entitled to suspend the Processing until the instruction is withdrawn or confirmed.
+Zitadel processes Personal Data in accordance with its privacy policy (cf. [Privacy Policy](./policies/privacy-policy)) and upon the documented directions of the Customer (which includes the Agreement).
+Subsequent instructions shall be given either in writing, whereby e-mail shall suffice, or orally with immediate written confirmation.
-### Obligation of the processing persons to confidentiality
+Zitadel shall promptly inform Customer if it becomes aware that such processing instructions infringe Applicable Data Protection Law (but without obligation to actively monitor compliance with Applicable Data Protection Law).
+In such case, Zitadel shall be entitled to suspend the processing until the infringing instruction is withdrawn or confirmed.
-The Processor shall ensure that the persons authorized to process the Personal Data have committed themselves to confidentiality, unless they are already subject to an appropriate statutory duty of confidentiality.
+### Confidentiality obligations
+
+Zitadel shall ensure that any person that it authorizes to process Customer Data (including Zitadel’s staff, agents and sub-processors) (an “Authorized Person”) shall be subject to a strict duty of confidentiality (whether a contractual duty or a statutory duty) and shall not permit any person to process the Customer Data that is not under such a duty of confidentiality.
+Zitadel shall ensure that all Authorised Persons process the Customer Data only as necessary for the Permitted Purpose.
### Technical and organizational measures
-The Processor has taken appropriate technical and organizational security measures, maintains them for the duration of the Processing and updates them on an ongoing basis in accordance with the current state of technology.
-
-The technical and organizational security measures are described in more detail in the [annex](#annex-regarding-security-measures) to this appendix.
+Zitadel shall implement appropriate technical and organizational measures to protect the Customer Data from a Security Incident, as described in Annex II to this DPA.
+Such measures shall comply with all Applicable Data Protection Law and shall further have regard to the state of the art, the costs of implementation and the nature, scope, context and purposes of processing as well as the risk of varying likelihood and severity for the rights and freedoms of natural persons.
+The Customer acknowledges that such measures are subject to technical progress and development and that Zitadel may update or modify such measures from time to time, provided that such updates and modifications do not degrade or diminish overall security of the Customer Data, or of the Services under the Agreement.
### Involvement of subcontracted processors
-A current and complete [list of involved and approved sub-processors](./subprocessors) can be found in our legal section.
+Customer agrees that Zitadel may engage sub-processors to process Customer Data on Customer’s behalf. A current and complete [list of involved and approved sub-processors](https://zitadel.com/trust) can be found on our [Trust Center](https://zitadel.com/trust) (as may be updated from time to time in accordance with this DPA).
-The Processor is entitled to involve additional sub-processors.
-In this case, the Processor shall inform the Responsible Party about any intended change regarding sub-processors and update the list of involved an approved sub-processors.
-The Customer has the right to object to such changes.
-If the Parties are unable to reach a mutual agreement within 30 days of receipt of the objection by the Processor, the Customer may terminate the Agreement extraordinarily.
+Zitadel will notify Customer by updating the list of sub-processors and, if Customer has subscribed to notices, via email.
+If, within five (5) calendar days after such notice, Customer notifies Zitadel in writing that Customer objects to Zitadel's appointment of a new sub-processor based on reasonable data protection concerns, the parties will discuss such concerns in good faith with a view to achieving a commercially reasonable resolution.
+If the parties are not able to mutually agree to a resolution of such concerns, Customer, as its sole and exclusive remedy, may terminate the Agreement for convenience with no refunds and Customer will remain liable to pay any committed fees in an order form, order, statement of work or other similar ordering document.
-The Processor obligates itself to impose on all sub-processors, by means of a contract (or in another appropriate manner), the same data protection obligations as are imposed on it by this Annex.
-In particular, sufficient guarantees shall be provided that the appropriate technical and organizational measures are implemented in such a way that the processing by the sub-processor is carried out in accordance with the legal requirements.
+Zitadel shall inform the Customer if it adds or replaces any sub-processor at least fifteen (15) days prior to any such change (including details of the processing it performs or will perform).
+The Customer may object in writing to Zitadel’s engagement of a new sub-processor on reasonable grounds relating to the protection of Customer Personal Data by notifying Zitadel promptly in writing within fifteen (15) calendar days of receipt of Zitadel’s notice.
+In such case, the parties shall discuss Customer’s concerns in good faith with a view to achieving a commercially reasonable resolution.
+If such objection right is not exercised by Customer, silence shall be deemed to constitute an approval of the relevant sub-processor engagement.
-Our websites and services may involve processing by third-party sub-processors with country of registration outside of Switzerland or the EU/EAA.
-In these cases, we only transfer personal data after we have implemented the legally required measures for this, such as concluding standard contractual clauses on data protection or obtaining the consent of the data subjects. If interested, the documentation on these measures can be obtained from the contact person mentioned above.
-The country of registration of a sub-processor may be different from the hosting location of the data. Please refer to the [list of involved and approved sub-processors](./subprocessors) for more details.
+Where Zitadel appoints a sub-processor, Zitadel shall: (i) enter into an agreement with each sub-processor containing data protection terms that provide at least the same level of protection for Customer Data as those contained in this DPA, to the extent applicable to the nature of the services provided by such sub-processor; and (ii) remain responsible to the Customer for Zitadel’s sub-processors’ failure to perform their obligations with respect to the processing of Customer Data.
-If the sub-processor fails to comply with its data protection obligations, the processor shall be liable to the customer for this as for its own conduct.
+Taking into account the safeguards set forth in this DPA, Customer Data may be processed outside of Switzerland or the EU/EAA, such as in the United States or any country in which Zitadel or is sub-processors operate. Our [list of involved and approved sub-processors](https://zitadel.com/trust) provides additional details.
### Assistance in responding to requests
-The Processor shall support the Customer as far as possible with suitable technical and organizational measures in fulfilling its obligation to respond to requests to exercise the data subject's rights (**"Data Subject Request"**).
-The Processor will promptly notify the Customer if it receives a Data Subject Request.
-The Processor will not respond to a Data Subject Request, provided that the Customer agrees the Processor may at its discretion respond to confirm that such request relates to the Customer.
-The Customer acknowledges and agrees that the Services include features which will allow the Customer to manage Data Subject Requests directly through the Services without additional assistance from the Processor.
-If the Customer does not have the ability to address a Data Subject Request, the Processor will, upon the Customer’s written request, provide reasonable assistance to facilitate the Customer’s response to the Data Subject Request to the extent such assistance is consistent with applicable law; provided that the Customer will be responsible for paying for any costs incurred or fees charged by the Processor for providing such assistance.
+Zitadel shall provide all reasonable and timely assistance (which may include by appropriate technical and organizational measures) to the Customer to enable the Customer to respond to: (i) any request from a data subject to exercise any of their rights under Applicable Data Protection Law ("**Data Subject Request**"); and (ii) any other correspondence, enquiry or complaint received from a data subject, regulator or other third party in connection with the processing of Customer Personal Data.
-The Processor, unless prohibited from doing so by applicable law, will promptly notify the Customer of any requests from a regulator or any other authority in relation to Personal Data that is being processed on behalf of the Customer, given that request resulted in disclosure of Personal Data to the regulator or any other authority.
+In the event that any such request, correspondence, enquiry or complaint is made directly to Zitadel, Zitadel shall promptly inform the Customer providing full details of the same.
+Zitadel will not respond to a Data Subject Request, however the Customer acknowledges and agrees that Zitadel may at its discretion respond to confirm that such request relates to the Customer.
-### Further support for the customer
+The Customer hereby acknowledges and agrees that the Services include features which will allow the Customer to manage Data Subject Requests directly through the Services without additional assistance from the Processor.
+If the Customer does not have the ability to address a Data Subject Request, Zitadel will, upon the Customer’s written request, provide reasonable assistance to facilitate the Customer’s response to such Data Subject Request to the extent such assistance is consistent with Applicable Data Protection Law; provided that the Customer will be responsible for paying for any reasonable costs incurred or fees charged by Zitadel for providing such assistance.
-The Processor shall, taking into account the nature of the processing and the information available to it, assist the Customer in complying with its obligations in connection with the security of the processing, any notifications of [Security Incidents](#security-incidents), and any data protection impact assessments.
+Zitadel, unless prohibited from doing so by applicable law, will promptly notify the Customer of any requests from a regulator, law enforcement authority or any other relevant and competent authority in relation to the Customer Personal Data that is being processed on behalf of the Customer, to the extent that the request may result in the disclosure of Customer Personal Data to such regulator, law enforcement authority or any other relevant and competent authority.
+
+### Cooperation and support for the Customer
+
+Zitadel shall provide the Customer with all such reasonable and timely assistance as Customer may require in order to enable it to conduct a data protection impact assessment (or equivalent document) where required by Applicable Data Protection Law, including, if necessary, to assist Customer to consult with its relevant data protection or other regulatory authority.
### Security incidents
-The Processor will notify the Customer of any incident, meaning breach of security or other action or inaction leading to the accidental or unlawful destruction, loss, alteration, unauthorised disclosure of, or access to, personal data covered under this (***Security Incident"**) without undue delay, and will promptly provide the Customer with all reasonable information concerning the Security Incident insofar as it affects the Customer.
-If possible, the Processor will promptly implement measures proposed in the notification.
-Insofar required the Processor will assist the Customer in notifying any applicable regulatory authority.
+Upon becoming aware of a Security Incident, Zitadel shall inform Customer without undue delay and provide all such timely information and cooperation as Customer may require for the Customer to fulfil its data breach or cybersecurity incident reporting obligations under (and in accordance with the timescales required by) Applicable Data Protection Law.
+Customer shall further take all such measures and actions as are reasonable and necessary to investigate, contain, and remediate or mitigate the effects of the Security Incident, to the extent that the remediation is within Zitadel's control, and shall keep Customer informed of all material developments in connection with the Security Incident.
+
+Notwithstanding anything to the contrary, Zitadel's notification of or response to a Security Incident under this section will not be construed as an acknowledgment by Zitadel of any fault or liability with respect to such Security Incident.
### Deletion or destruction after termination
-Upon Customer's request, the Processor shall delete personal data received after the end of the agreement, unless there is a legal obligation for the Processor to store or further process such data.
+Upon termination or expiry of the Agreement, Zitadel shall (at the Customer’s election) destroy or return to the Customer all Customer Data (including all copies of the Customer Data) in its possession or control (including any Customer Data subcontracted to a third party for processing).
+This requirement shall not apply to the extent that Zitadel is required by any applicable law to retain some or all Customer Data, in which case Zitadel shall isolate and protect the Customer Data from any further processing except to the extent required by such law until deletion is possible.
-### Information and control rights of the customer
+### Customer's information and audit rights
-The Processor shall provide the Customer with all information necessary to demonstrate compliance with the obligations set forth in this annex or to respond to requests from an applicable supervisory authority, subject to the confidentiality terms in the Framework Agreement.
-The Processor shall enable and contribute to audits, including inspections, carried out by the Customer or an auditor appointed by the Customer.
+To the extent required under Applicable Data Protection Law and on written request from the Customer, Zitadel shall provide written responses (which may include audit report summaries/extracts) to all reasonable requests for information made by the Customer related to its processing of Customer Personal Data as necessary to confirm Zitadel's compliance with this DPA.
+The Customer shall not exercise this right more than once in any twelve (12)-month rolling period, except (i) if and when required by instruction of a competent data protection or other regulatory authority; or (ii) if Zitadel has experienced a Security Incident where Customer was directly impacted.
-The procedure to be followed in the event of directions that are presumed to be unlawful is governed by the section [Bound by directions](#bound-by-directions) of this Appendix.
+Nothing in this section shall be construed to require Zitadel to document or provide: (i) trade secrets or any proprietary information; (ii) any information that would violate Zitadel’s confidentiality obligations, contractual obligations, or applicable law; or (iii) any information, the disclosure of which could threaten, compromise, or otherwise put at risk the security, confidentiality, or integrity of Zitadel’s infrastructure, networks, systems, algorithms or data.
-## Annex regarding security measures
+### Service Optimization
-The Processor has taken the following organizational and technical security measures to ensure a level of protection of the Personal Data processed that is appropriate to the risk:
+Where permitted by Applicable Data Protection Law, Zitadel may process Customer Data: (i) for its internal uses to build or improve the quality of its services; (ii) to detect Security Incidents; and (iii) to protect against fraudulent or illegal activity.
+
+Zitadel may: (i) compile aggregated and/or de-identified information in connection with the provision of the Services, provided that such information cannot reasonably be used to identify Customer or any data subject to whom Customer Personal Data relates (“Aggregated and/or De-Identified Data”); and (ii) use such Aggregated and/or De-Identified Data for its lawful business purposes in accordance with Applicable Data Protection Law.
+
+### Data Transfers
+
+Where either Party intends to transfer Personal Data cross-border and Applicable Data Protection Law requires certain measures to be implemented prior to such transfer, each Party agrees to implement such measures to ensure compliance with Applicable Data Protection Law.
+
+To the extent that the transfer of Personal Data from Customer to Zitadel involves a transfer of Personal Data outside the European Economic Area (EEA), Switzerland, or the United Kingdom to a jurisdiction which is not subject to an adequacy determination by the European Commission, United Kingdom or Swiss authorities (as applicable) that covers such transfer, then the SCCs are hereby incorporated by reference and form an integral part of the DPA.
+
+#### EEA Transfers
+
+To the extent that Customer Personal Data is subject to the GDPR, and the transfer would be a Restricted Transfer, the SCCs apply as follows:
+
+1) the Customer is the ‘data exporter’ and Zitadel is the ‘data importer’;
+2) the Module Two terms (Transfer controller to processor) apply;
+3) in Clause 7, the optional docking clause does not apply;
+4) in Clause 9, Option 2 (General Authorization) applies and the time period for prior notice of sub-processor changes is set out in this DPA;
+5) in Clause 11, the optional language does not apply;
+6) in Clause 17, Option 1 applies, and the SCCs are governed by German law;
+7) in Clause 18(b), disputes will be resolved before the courts of Hamburg in Germany;
+8) in Annex I, the details of the parties and the transfer are set out in the Agreement;
+9) in Clause 13(a) and Annex I, the Hamburg data protection authority will act as competent supervisory authority;
+10) in Annex II, the description of the technical and organizational security measures is set out in Annex 2 of this DPA or, if not set out therein, the applicable statement of work; and
+11) in Annex III, the list of sub-processors is set out at the address [https://zitadel.com/trust](https://zitadel.com/trust) or, if not set out therein, applicable statement of work.
+
+#### Swiss Transfers
+
+To the extent that Customer Personal Data is subject to Swiss law, and the transfer would be a Restricted Transfer, the SCCs apply as set out above with the following modifications:
+
+1) references to ‘Regulation (EU) 2016/679’ are interpreted as references to the Swiss FADP or any successor thereof;
+2) references to specific articles of ‘Regulation (EU) 2016/679’ are replaced with the equivalent article or section of the Swiss FADP,
+3) references to ‘EU’, ‘Union’ and ‘Member State’ are replaced with ‘Switzerland’,
+4) Clause 13(a) and Part C of Annex 2 is not used and the ‘competent supervisory authority’ is the Swiss Federal Data Protection Information Commissioner (“**FDPIC**”) or, if the transfer is subject to both the Swiss FADP and the GDPR, the FDPIC (insofar as the transfer is governed by the Swiss FADP) or the DPC (insofar as the transfer is governed by the GDPR),
+5) references to the ‘competent supervisory authority’ and ‘competent courts’ are replaced with the FDPIC and ‘competent Swiss courts’,
+6) in Clause 17, the SCCs are governed by the laws of Switzerland,
+7) in Clause 18(b), disputes will be resolved before the competent Swiss courts, and
+8) the SCCs also protect the data of legal entities until entry into force of the revised Swiss FADP.
+
+#### UK Transfers
+
+To the extent that Customer Personal Data is subject to Applicable Data Protection Law of the United Kingdom, and the transfer would be a Restricted Transfer, the SCCs as set out above shall apply as amended by Part 2 of the UK Addendum, and Part 1 of the UK Addendum is deemed completed as follows:
+
+1) in Table 1, the details of the parties are set out in the Agreement or, if not set out therein, the applicable statement of work;
+2) in Table 2, the selected modules and clauses are set out in Section 6.3 of this DPA;
+3) in Table 3, the appendix information is set out in the annexes to this DPA or, if not set out therein, the applicable statement of work; and
+4) in Table 4, the ‘Exporter’ is selected.
+
+#### Alternative Transfer Mechanism
+
+In the event that a court of competent jurisdiction or supervisory authority orders (for whatever reason) that the measures described in this DPA cannot be relied on to lawfully transfer Customer Personal Data, or Zitadel adopts an alternative data transfer mechanism to the mechanisms described in this DPA, including any new version of or successor to the standard contractual clauses (“Alternative Transfer Mechanism”), the Customer agrees to fully co-operate with Zitadel to agree an amendment to this DPA and/or execute such other documents and take such other actions as may be necessary to remedy such non-compliance or give legal effect to such Alternative Transfer Mechanism.
+
+### Additional Provisions under US Data Protection Laws
+
+The Parties agree that all Customer Personal Data that is subject to US Data Protection Laws (including the CCPA) is disclosed to Zitadel by the Customer for the Permitted Purpose and its use or sharing by the Customer with Zitadel is necessary to perform such Permitted Purpose.
+
+Zitadel agrees that it will not:
+
+1. sell or share any Customer Personal Data to a third party for any purpose other than than for the Permitted Purpose;
+2. retain, use, or disclose any Customer Personal Data (i) for any purpose other than for the Permitted Purpose, including for any commercial purpose, or (ii) outside of the direct business relationship between the Parties, except as necessary to perform the Permitted Purpose or as otherwise permitted by US Data Protection Laws; or
+3. combine Customer Personal Data received from or on behalf of Customer with Personal Data received from or on behalf of any third party or collected from Zitadel’s own interaction with individuals or data subjects, except to perform a Permitted Purpose in accordance with the CCPA, the Agreement and this DPA.
+
+The Parties acknowledge that the Customer Personal Data that Customer discloses to Zitadel is provided only for the limited and specified purposes set forth as the Permitted Purpose in the Agreement and this DPA.
+
+Zitadel shall provide the same level of protection to Customer Personal Data as required by the CCPA and will: (i) assist the Customer in responding to any request from a data subject to exercise rights under US Data Protection Laws; and (ii) immediately notify the Customer if it is not able to meet the requirements under the CCPA.
+
+The Customer may take such reasonable and appropriate steps as may be necessary (a) to ensure that the Customer Personal Data collected is used in a manner consistent with the business’s obligations under the CCPA; and (b) to stop and remediate any unauthorized use of Customer Personal Data, and (b) to ensure that Customer Personal Data is used in a manner consistent with the CCPA.
+
+### Miscellaneous
+
+This DPA shall be governed by and construed in accordance with the governing law and jurisdiction provisions set out in the Agreement, unless required otherwise by Applicable Data Protection Law.
+
+Any liability owed by one party to the other under this DPA shall be subject to the limitations of liability set forth in the Agreement.
+
+This DPA shall terminate upon the earlier of (i) the termination or expiry of all Agreement under which Customer Data may be processed, or (ii) the written agreement of the Parties.
+
+Any notices shall be delivered to a Party in accordance with the notice provisions of the Agreement, unless otherwise specified hereunder.
+
+## Annex 1: Description of Processing Activities / Transfer
+
+### List of Parties
+
+| Data Exporter | Data Importer |
+| :---- | :---- |
+| Name: The Party identified as the Customer in the Agreement. | Name: The Party identified as Zitadel in the Agreement. |
+| Address: As identified in the Agreement. | Address: As identified in the Agreement. |
+| Contact Person's Name, position and contact details: As identified in the Agreement. | Contact Person's Name, position and contact details: As identified in the Agreement. |
+| Activities relevant to the transfer: See below | Activities relevant to the transfer: See below |
+| Role: Controller | Role: Processor |
+
+### Description of processing / transfer
+
+| | Description |
+| :---- | :---- |
+| **Categories of data subjects:** | As described in the section "Processing of Personal Data" of the DPA |
+| **Categories of personal data:** | As described in the section "Processing of Personal Data" of the DPA |
+| **Sensitive data:** | None. |
+| **If sensitive data, the applied restrictions or safeguards** | N/A |
+| **Frequency of the transfer:** | Continuous |
+| **Nature and subject matter of processing:** | The Services described in the Agreement. |
+| **Purpose(s) of the data transfer and further processing:** | As set forth in the Agreement. |
+| **Retention period (or, if not possible to determine, the criteria used to determine that period):** | The personal data may be retained until termination or expiry of the DPA. |
+
+### Competent supervisory authority
+
+The competent supervisory authority in connection with Customer Personal Data protected by the GDPR, is the Hamburg data protection authority.
+If this is not possible, then as otherwise agreed by the parties consistent with the conditions set forth in Clause 13.
+
+In connection with Customer Personal Data that is protected by UK-GDPR, the competent supervisory authority is the Information Commissioners Office (the "ICO").
+
+## Annex 2: Technical and organizational measures
+
+Zitadel has implemented an information security program, that is designed to protect the confidentiality, integrity and availability of Customer Data. Zitadel's information security program includes the following organizational and technical security measures to ensure a level of protection of the Personal Data processed that is appropriate to the risk:
### Pseudonymization / Encryption
The following measures for pseudonymization and encryption exist:
-1. All communication is encrypted with TLS >1.2 with PFS
+1. All communication is encrypted with TLS >1.2 with PFS
2. Critical data is exclusively stored in encrypted form
3. Storage media that store customer data are always encrypted
4. Passwords are irreversibly stored with a hash function
diff --git a/docs/docs/legal/policies/privacy-policy.mdx b/docs/docs/legal/policies/privacy-policy.mdx
index 30e213adb0..3dc544f8ae 100644
--- a/docs/docs/legal/policies/privacy-policy.mdx
+++ b/docs/docs/legal/policies/privacy-policy.mdx
@@ -2,20 +2,42 @@
title: Privacy Policy
custom_edit_url: null
---
-import PiidTable from '../_piid-table.mdx';
-Last updated on March 07, 2024
+Last updated on 20 March, 2025
-This privacy policy applies to CAOS Ltd., the websites it operates (including zitadel.ch, zitadel.cloud and zitadel.com) and the services and products it provides (including ZITADEL). This privacy policy describes how we process personal data for the provision of this websites and our products.
+This privacy policy describes how ZITADEL Inc. and its wholly owned subsidiaries and affiliates (collectively, "**ZITADEL**", “**CAOS**", "**we**" or "**us**") collect, use, disclose and otherwise process your personal data in connection with the management of our business and our relationships with customers, visitors and event attendees.
-If any inconsistencies arise between this Privacy Policy and the otherwise applicable contractual terms, framework agreement, or general terms of service, the provisions of this Privacy Policy shall prevail. This privacy policy covers both existing personal data and personal data collected from you in the future.
+This privacy policy explains your rights and choices related to the personal data we collect when:
-The responsible party for the data processing described in this privacy policy and contact for questions and issues regarding data protection is
+* You interact with our websites, including zitadel.com, zitadel.cloud and zitadel.ch as well any other websites that we operate and that link to this privacy policy (our “**Sites**”)
-**CAOS AG**
+* You visit, interact with, or use any of our offices, events, sales, marketing or other activities; and
+
+* You use our platform, including ZITADEL and our software, mobile application, and other products and services (the “**Services**”).
+
+This privacy policy does not cover:
+
+* **Organizational Use**. When you use our Services on behalf of an organization (your employer), your use is administered and provisioned by your organization under its policies regarding the use and protection of personal data. If you have questions about how your data is being accessed or used by your organization, please refer to your organization's privacy policy and direct your inquiries to your organization's system administrator.
+
+* **Third Parties**. Our Sites include links to websites and/or applications operated and maintained by third parties (e.g. GitHub, LinkedIn, etc.). This privacy policy does not apply to any products, services, websites, or content that are offered by third parties and/or have their own privacy policy.
+
+If any inconsistencies arise between this privacy policy and the otherwise applicable contractual terms, framework agreement, or general terms of service, the provisions of this privacy policy shall prevail (where applicable). This privacy policy covers both existing personal data and personal data which may be collected from you in the future.
+
+ZITADEL determines the purposes for and means of the processing (i.e., we are the data controller) of your personal data as described in this privacy policy, unless expressly specified otherwise. The responsible party for the data processing described in this privacy policy and contact for questions and issues regarding data protection is:
+
+**Zitadel Inc.**
+Data Protection Officer
+Four Embarcadero Center, Suite 1400
+San Francisco, CA 94111-4164
+United States of America
+[legal@zitadel.com](mailto:legal@zitadel.com)
+
+**CAOS AG (Affiliate of Zitadel, Inc.)**
Data Protection Officer
Lerchenfeldstrasse 3
-9014 St. Gallen
+9014 St. Gallen
+Switzerland
+[legal@zitadel.com](mailto:legal@zitadel.com)
Switzerland
[legal@zitadel.com](mailto:legal@zitadel.com)
@@ -41,15 +63,13 @@ This website uses TLS encryption for security reasons and to protect the transmi
We process personal data in accordance with Swiss data protection law. In addition, we process - to the extent and insofar as the EU Data Protection Regulation is applicable - personal data in accordance with the following legal bases within the meaning of Art. 6 (1) DSGVO :
-- Insofar as we obtain the consent of the data subject for processing operations, Art. 6 (1) a) DSGVO serves as the legal basis.
-- When processing personal data for the fulfillment of a contract with the data subject as well as for the implementation of corresponding pre-contractual measures, Art. 6 para. 1 lit. b DSGVO serves as the legal basis.
-- To the extent that processing of personal data is necessary to comply with a legal obligation to which we are subject under any applicable law of the EU or under any applicable law of a country in which the GDPR applies in whole or in part, Art. 6 para. 1 lit. c GDPR serves as the legal basis.
-- For the processing of personal data in order to protect vital interests of the data subject or another natural person, Art. 6 para. 1 lit. d DSGVO serves as the legal basis.
-- If personal data is processed in order to protect the legitimate interests of us or of third parties and if the fundamental freedoms and rights and interests of the data subject do not override our interests and the interests of third parties, Article 6 (1) (f) of the GDPR serves as the legal basis. Legitimate interests are in particular our business interest in being able to provide our website and our products, information security, the enforcement of our own legal claims and compliance with Swiss law.
+* Insofar as we obtain the consent of the data subject for processing operations, Art. 6 (1) a) DSGVO serves as the legal basis.
+* When processing personal data for the fulfillment of a contract with the data subject as well as for the implementation of corresponding pre-contractual measures, Art. 6 para. 1 lit. b DSGVO serves as the legal basis.
+* To the extent that processing of personal data is necessary to comply with a legal obligation to which we are subject under any applicable law of the EU or under any applicable law of a country in which the GDPR applies in whole or in part, Art. 6 para. 1 lit. c GDPR serves as the legal basis.
+* For the processing of personal data in order to protect vital interests of the data subject or another natural person, Art. 6 para. 1 lit. d DSGVO serves as the legal basis.
+* If personal data is processed in order to protect the legitimate interests of us or of third parties and if the fundamental freedoms and rights and interests of the data subject do not override our interests and the interests of third parties, Article 6 (1) (f) of the GDPR serves as the legal basis. Legitimate interests are in particular our business interest in being able to provide our website and our products, information security, the enforcement of our own legal claims and compliance with Swiss law.
-We will retain personal data for the period of time necessary for the particular purpose for which it was collected.
-
-Subsequently, they are either deleted or made anonymous, unless we need them for a longer period of time in exceptional cases, e.g. due to legal storage and documentation obligations or our legitimate interests, such as the protection of rights to which we are entitled or the defense of claims.
+We will retain personal data for the period of time necessary for the particular purpose for which it was collected and where we have an ongoing legitimate business need to do so (for example to comply with applicable legal, tax or accounting requirements). Subsequently, they are either deleted or made anonymous, unless we need them for a longer period of time in exceptional cases, e.g. due to legal storage and documentation obligations or our legitimate interests, such as the protection of rights to which we are entitled or the defense of claims.
### Processing of personal data when using the website, contact forms and in connection with newsletters
@@ -57,45 +77,49 @@ Our websites can generally be visited without registration. Each time one of our
This data is processed to enable correct delivery and functioning of the website. In addition, we use the data to optimize the website and to ensure the security of our systems.
-Personal data, in particular name, address or e-mail address are collected as far as possible on a voluntary basis, for example when you contact us via a contact form or by e-mail. Without your consent, the data will not be passed on to third parties, unless shown in this privacy policy.
+Personal data, in particular name, address or e-mail address are collected as far as possible on a voluntary basis, for example when you contact us via a contact form or by e-mail. Without your consent, the data will not be passed on to third parties, unless otherwise stated in this privacy policy.
If you send us inquiries via contact form, your data from the form, including any data you provided, will be stored by us for the purpose of processing the inquiry and in case of follow-up questions. We do not pass on this data without your consent, except insofar as this is shown in this privacy policy.
-If you would like to receive newsletters offered on our websites, we require an e-mail address from you as well as information that allows us to verify that you are the owner of the specified e-mail address and agree to receive the newsletter. Further data will not be collected. We use this data exclusively for sending the requested information and do not pass it on to third parties, except as described in this privacy policy.
+If you would like to receive newsletters offered on our Sites, we require an e-mail address from you as well as information that allows us to verify that you are the owner of the specified e-mail address and agree to receive the newsletter. Further data will not be collected. We use this data exclusively for sending the requested information and do not pass it on to third parties, except as described in this privacy policy.
You can revoke your consent to the storage of the data, the e-mail address and their use for sending the newsletter at any time, for example via the "unsubscribe link" in the newsletter.
-### Processing of personal data in connection with the use of our products
+### Processing of personal data when applying for a job with us
+
+Our Sites can generally be visited without registration. If you apply for a job with us, we may collect and process according to the [Privacy policy for the ZITADEL employer branding and recruitment](https://jobs.zitadel.com/privacy-policy). You may request and delete your data with the links on our [data & privacy page](https://jobs.zitadel.com/data-privacy).
+
+### Processing of personal data in connection with the use of our Services
The use of our services is generally only possible with registration. During registration and in the course of using the services, we collect and process various personal data.
In particular, the following personal data are part of the processing:
-
+
+import { PiiTable } from "../../../src/components/pii_table";
+
+
Unless otherwise mentioned, the nature and purpose of the processing is as follows:
-The data is uploaded by customers in our services or collected by us based on requests from users. The personal data is processed by us exclusively for the provision of the requested services or the use of the agreed services.
+The data is uploaded by customers in our Services or collected by us based on requests from users. The personal data is processed by us exclusively for the provision of the requested Services or the use of the agreed Services.
The fulfillment of the contract includes in particular, but is not limited to, the processing of personal data for the purpose of:
-- Authentication and authorization of users
-- Storage and processing of user actions in the audit trail
-- Processing of personal data and login information
-- Verification of communication means
-- Communication regarding service interruptions or service changes
+* Authentication and authorization of users
+* Storage and processing of user actions in the audit trail
+* Processing of personal data and login information
+* Verification of communication means
+* Communication regarding service interruptions or service changes
## Disclosure to third parties
### Third party sub-processors
-We use third-party services to provide the website and our offers. An up-to-date list of all the providers we use and their areas of activity can be found on our [list of involved and approved sub-processors](../subprocessors).
+We use third-party services to provide the website and our offers. An up-to-date list of all the providers we use and their areas of activity can be found on our [Trust Center](/trust).
### External payment providers
-This website uses external payment service providers through whose platforms users and we can make payment transactions. For example via
-
-- [Stripe](https://stripe.com/ch/privacy)
-- [Bexio AG](https://www.bexio.com/de-CH/datenschutz)
+This Site uses external payment service providers through whose platforms users and we can make payment transactions. For example, via [Stripe](https://stripe.com/ch/privacy).
As an alternative, we offer customers the option to pay by invoice instead of using external payment providers. However, this may require a positive credit check in advance.
@@ -105,91 +129,166 @@ For payment transactions, the terms and conditions and the data protection notic
### Law enforcement
-We disclose personal information to law enforcement agencies, investigative authorities or in legal proceedings to the extent we are required to do so by law or when necessary to protect our rights or the rights of users.
+We disclose personal data to law enforcement agencies, investigative authorities or in legal proceedings to the extent we are required to do so by law or when necessary to protect our rights or the rights of users.
## Cookies
-Our websites use cookies. These are small text files that make it possible to store specific information related to the user on the user's terminal device while the user is using the website. Cookies enable us, in particular, to offer a single sign-on procedure, to control the performance of our services, but also to make our offer more customer-friendly. Cookies remain stored beyond the end of a browser session and can be retrieved when the user visits the site again.
+Our Sites use cookies. These are small text files that make it possible to store specific information related to the user on the user's terminal device while the user is using the website. Cookies enable us, in particular, to offer a single sign-on procedure, to control the performance of our Services, but also to make our offer more customer-friendly. Cookies remain stored beyond the end of a browser session and can be retrieved when the user visits the site again.
-In particular, we use the following cookies to provide our services:
-
-When you use our services, we may collect information about your visit, including via cookies, beacons, invisible tags, and similar technologies (collectively “cookies”) in your browser and on emails sent to you.
-This information may include Personal Information, such as your IP address, web browser, device type, and the web pages that you visit just before or just after you use the services, as well as information about your interactions with the services, such as the date and time of your visit, and where you have clicked.
+When you use our Services, we may collect information about your visit, including via cookies, beacons, invisible tags, and similar technologies (collectively “cookies”) in your browser and on emails sent to you. This information may include personal data, such as your IP address, web browser, device type, and the web pages that you visit just before or just after you use the Services, as well as information about your interactions with the Services, such as the date and time of your visit, and where you have clicked.
### Necessary cookies
-Some cookies are strictly necessary to make our services available to you.
-We cannot provide you with our services without this type of cookies.
+Some cookies are strictly necessary to make our Services available to you. We cannot provide you with our Services without this type of cookies.
Necessary cookies provide basic functionality such as:
-- Session Management
-- Single Sign-On
-- Rate Limiting
-- DDoS Mitigation
-- Remembering Preferences
+* Session Management
+* Single Sign-On
+* Rate Limiting
+* DDoS Mitigation
+* Remembering Preferences
### Analytical cookies
-We also use cookies for website analytics purposes in order to operate, maintain, and improve the services for you.
-We use Google Analytics 4 to collect and process certain analytics data on our behalf.
-Google Analytics helps us understand how you engage with the services and may also collect information about your use of other websites, apps, and online resources.
-We don't use google analytics on customer instances of ZITADEL, only on our public websites and customer portal.
+We also use cookies for website analytics purposes in order to operate, maintain, and improve the Services for you. We use Google Analytics 4 and PostHog to collect and process certain analytics data on our behalf. Google Analytics and PostHog helps us understand how you engage with the Services and may also collect information about your use of other websites, apps, and online resources.
-You can learn about Google’s practices by going to https://www.google.com/policies/privacy/partners/ and opt out by managing your cookie consent through our services or an third-party tool of your choice.
+You can learn about the analytics providers' practices by going to
-If you do not want us to use cookies during your visit, you can disable their use in your browser settings.
-In this case, certain parts of our website (e.g. language selection) may not function or may not function fully.
-Where required by applicable law, we obtain your consent to use cookies.
+* [https://www.google.com/policies/privacy/partners/](https://www.google.com/policies/privacy/partners/)
+* [https://posthog.com/privacy](https://posthog.com/privacy)
+* [https://legal.hubspot.com/privacy-policy](https://legal.hubspot.com/privacy-policy)
+* [https://www.commonroom.io/privacy-policy/](https://www.commonroom.io/privacy-policy/)
+
+and opt out by managing your cookie consent through our Services or a third-party tool of your choice.
+
+If you do not want us to use cookies during your visit, you can disable their use in your browser settings. In this case, certain parts of our Sites (e.g. language selection) may not function or may not function fully. Where required by applicable law, we obtain your consent to use cookies.
+
+## How we protect personal data
+
+Personal data is maintained on our servers or those of our service providers, and is accessible by authorized employees, representatives, and agents as necessary for the purposes described in this privacy policy.
+
+We maintain a range of physical, electronic, and procedural safeguards designed to help protect personal data. While we attempt to protect your personal data in our possession, we cannot guarantee at all times the security of the data as no method of transmission over the internet or security system is perfect.
+
+If you choose to remain logged in, you should be aware that anyone with access to your device will be able to access your account and we therefore strongly recommend that you take appropriate steps to protect against unauthorized access to, and use, of your account. Please also notify us as soon as possible if you suspect any unauthorized use of your account or password.
## Rights of data subjects
+Depending on your location and subject to applicable law, you may have the following rights regarding the personal data we process:
+
### Right to information
-Any person affected by the processing has the right to obtain information from the responsible data processor at any time about the personal data stored about him or her.
+You have the right to know what personal data we hold and process about you and to access such personal data.
### Right to rectification
-Every person affected by the processing has the right to demand the correction of inaccurate personal data concerning him or her. Furthermore, the data subject has the right to request the completion of incomplete personal data, taking into account the purposes of the processing.
+You have the right to request the correction of inaccurate personal data concerning you.
### Right to erasure (right to be forgotten)
-Any person affected by the processing has the right, in certain cases, to request from the responsible data processor to delete the personal data concerning him or her.
+You have the right to request the deletion or erasure of the personal data concerning you.
### Right to restrict processing
-Every person affected by the processing has the right in certain cases to request from the responsible data processor to restrict the processing.
+You have the right to request to restrict the processing of your personal data in certain cases.
### Right to data portability
-Every person affected by the processing has the right to receive the personal data concerning him or her in a structured, common and machine-readable format. He or she also has the right to have this data transferred to another data processor if the legal requirements are met.
+You have the right to receive the personal data concerning you in a structured, common and machine-readable format, and to have this data transferred to another data processor if the legal requirements are met.
### Right to object
-Every person affected by the processing has the right to object to the processing of personal data concerning him or her, insofar as we base the processing of his or her personal data on a balancing of interests. This is the case if the processing is not necessary, for example, to fulfill a contract or a legal obligation.
+Depending on the circumstances, you have the right to object to the processing of personal data concerning you, insofar as we base the processing of your personal data on a balancing of interests. This is the case if the processing is not necessary, for example, to fulfill a contract or a legal obligation.
-To exercise such an objection, the data subject must explain his or her reasons why we should not process his or her personal data as we have done. We will then review the situation and either stop or adjust the data processing or show the data subject our reasons for continuing the processing.
+To exercise such an objection, please indicate your reasons why we should not process your personal data as we have done. We will then review the situation and either stop or adjust the data processing or explain our reasons for continuing the processing.
### Right to revoke consent under data protection law
-Insofar as our processing is based on consent, the data subject has the right to revoke this consent at any time with effect for the future.
+Insofar as our processing is based on consent, you have the right to revoke your consent at any time with effect. Withdrawing your consent will not affect the lawfulness of any processing we conducted prior to your withdrawal, nor will it affect processing of your personal data conducted in reliance on lawful processing grounds other than consent.
### Assertion of rights by the data subjects
If you wish to exercise your rights, you may do so by contacting the above-mentioned contact person.
-A data subject also has the right to lodge a complaint with the competent data protection authority. The competent data protection authority in Switzerland is the Federal Data Protection and Information Commissioner (www.edoeb.admin.ch). The competent data protection authorities of EU countries can be viewed at this link: [https://ec.europa.eu/justice/article-29/structure/data-protection-authorities/index\_en.htm](https://ec.europa.eu/justice/article-29/structure/data-protection-authorities/index_en.htm)
+You can opt out of receiving marketing emails from us by following the unsubscribe link in the emails or by emailing us. If you choose to no longer receive marketing information, we may still communicate with you regarding such things as your security updates, product functionality, responses to service requests, or other transactional, non-marketing purposes.
-## Note on data transfer abroad
+If you have a concern about how we collect and use personal data, please contact us using the contact details provided at the beginning of this privacy policy. You also have the right to contact your local data protection authority if you prefer, such as:
-Our websites and services make use of tools from companies based in countries outside of Switzerland or the EU/EEA, namely those based in the USA. When these tools are active, your personal data may be transferred to the servers of the respective companies abroad. We would like to point out that some of these countries, namely the USA, are not a safe third country in the sense of Swiss and EU data protection law. In these cases, we only transfer personal data after we have implemented the legally required measures for this, such as concluding standard contractual clauses on data protection or obtaining the consent of the data subjects. If interested, the documentation on these measures can be obtained from the contact person mentioned above.
+* Data protection authorities in the European Economic Area (EEA): [https://edpb.europa.eu/about-edpb/board/members\_en](https://edpb.europa.eu/about-edpb/board/members_en);
+* Swiss data protection authorities: [https://www.edoeb.admin.ch/edoeb/en/home/deredoeb/kontakt.html](https://www.edoeb.admin.ch/edoeb/en/home/deredoeb/kontakt.html);
+* UK data protection authority: [https://ico.org.uk/global/contact-us/](https://ico.org.uk/global/contact-us/).
+
+## Additional Information for U.S. Residents
+
+Categories of personal data we collect and our purposes for collection and use
+You can find a list of the categories of personal data that we collect in the section above titled “Processing of personal data, legal basis, storage period”. In the last 12 months, we collected the following categories of personal data depending on the Services used:
+
+* Identifiers and account information, such as the username and email address;
+* Commercial information, such as information about transactions undertaken with us;
+* Internet or other electronic network activity information, such as information about activity on our Site and Services.
+* Geolocation information based on the IP address.
+* Audiovisual information in pictures, audio, or video content that you may choose to submit to us.
+* Professional or employment-related information or demographic information, but only if you explicitly provide it to us, such as by filling out a survey or by applying for a job with us.
+* Inferences we make based on other collected data, for purposes such as recommending content and analytics.
+
+For details regarding the sources from which we obtain personal data, please see the “Processing of personal data, legal basis, storage period” section above.
+We collect and use personal data for the business or commercial purposes described in the “Processing of personal data, legal basis, storage period” section above.
+
+Categories of personal data disclosed and categories of recipients
+
+We disclose the following categories of personal data for business or commercial purposes to the categories of recipients listed below:
+
+* We disclose identifiers with businesses, service providers, and third parties, such as analytics providers and social media networks.
+ * We disclose Internet or other network activity with businesses, service providers, and third parties, such as analytics providers and social media networks.
+ * We disclose geolocation information with businesses, service providers, and third parties such as advertising networks, analytics, and social media.
+ * We disclose payment information with businesses and service providers who process payments.
+ * We disclose commercial information with businesses, service providers, and third parties, such as analytics providers and social media networks.
+ * We disclose audiovisual information with businesses and service providers who help administer customer service and fraud or loss prevention services.
+ * We disclose inferences with businesses and service providers who help administer marketing and personalization.
+
+### Privacy rights
+
+Right to Opt-Out of Cookies and Sale/Sharing: Although we do not sell personal data for monetary value, our use of cookies and automated technologies may be considered a “sale” / “sharing” in certain states, such as California. Visitors to our US website can opt out of such third parties by clicking the “Manage cookie preferences” link at the bottom of our Site. The categories of personal data disclosed that may be considered a “sale” / “sharing” include identifiers, device information, Internet or other network activity, geolocation data, and commercial data.
+
+The categories of third parties to whom personal data was disclosed that may be considered “sale”/ “sharing” include data analytics providers and social media networks.
+
+We do not have actual knowledge that we sell or share the personal data of individuals under 16 years of age.
+
+If you are a resident of the State of Nevada, Chapter 603A of the Nevada Revised Statutes permits a Nevada resident to opt out of future sales of certain covered information that a website operator has collected or will collect about the resident. Although we do not currently sell covered information, please contact us to submit such a request.
+
+Right to Limit the Use of Sensitive Personal Information: We only collect sensitive personal information, as defined by applicable privacy laws, for the purposes allowed by law or with your consent. We do not use or disclose sensitive personal information except to provide you the Services or as otherwise permitted by law. We do not collect or process sensitive personal information for the purpose of inferring characteristics.
+
+Right to Access, Correct, and Delete Personal Data: Depending on your state of residence in the U.S., you may have:
+(i) the right to request access to and receive details about the personal data we maintain and how we have processed it, including the categories of personal data, the categories of sources from which personal data is collected, the business or commercial purpose for collecting, selling, or sharing personal data, the categories of third parties to whom personal data is disclosed, and the specific pieces of personal data collected;
+(ii) the right to delete personal data collected, subject to certain exceptions;
+(iii) the right to correct inaccurate personal data.
+
+When you make a request, we will verify your identity by asking you to sign into your account or if necessary by requesting additional information from you. You may also make a request using an authorized agent. If you submit a rights request through an authorized agent, we may ask such agent to provide proof that you gave a signed permission to submit the request to exercise privacy rights on your behalf. We may also require you to verify your own identity directly with us or confirm to us that you otherwise provided such agent permission to submit the request. Once you have submitted your request, we will respond within the time frame permitted by the applicable law.
+
+If you have any questions or concerns, you may reach us by contacting using one of the contact details listed at the beginning of this privacy policy.
+
+Depending on your state of residence, you may be able to appeal our decision to your request regarding your personal data. To do so, please contact us by using one of the contact details listed at the beginning of this privacy policy. We respond to all appeal requests as soon as we reasonably can, and no later than legally required.
+
+We do not discriminate against customers who exercise any of their rights described in our privacy policy.
+
+California Shine the Light: Customers who are residents of California may request information concerning the categories of personal data (if any) we disclose to third parties or affiliates for their direct marketing purposes. If you would like more information, please submit a written request to us by using one of the contact details listed at the beginning of this privacy policy.
+
+Do Not Track signals: Most modern web browsers give you the option to send a 'Do Not Track' signal to the sites you visit, indicating that you do not wish to be tracked. However, there is currently no accepted standard for how a site should respond to this signal, and we do not take any action in response to this signal.
+
+## Note on international data transfers
+
+Our Sites and Services make use of tools from companies based in countries outside of Switzerland or the EU/EEA, namely those based in the USA. When these tools are active, your personal data may be transferred to the servers of the respective companies abroad. If you are using the Site or Services from outside the United States, your personal data may be processed in a foreign country, where privacy laws may be less stringent than the laws in your country. In these cases, we only transfer personal data after we have implemented the legally required measures for this, such as concluding standard contractual clauses on data protection or obtaining the consent of the data subjects. If interested, the documentation on these measures can be obtained from the contact person mentioned above. By submitting your personal data to us you agree to the transfer, storage, and processing of your personal data in a country other than your country of residence including, but not necessarily limited to, the United States.
We actively try to minimize the use of tools from companies located in countries without equivalent data protection, however, due to the lack of alternatives, this is currently not always feasible without major inconvenience. If you have any concerns, please contact us directly and we will try to find a mutual solution for your needs.
-## Changes
+## Children's Privacy
-We may amend this privacy policy at any time without prior notice. Always the current version published on our website applies to users and customers of our website and services. Insofar as the data protection declaration is part of an agreement with you, we will inform you of the change by e-mail or other suitable means in the event of an update.
+Our Site is not intended for or directed to children under the age of 14. We do not knowingly collect personal data directly from children under the age of 14 without parental consent. If we become aware that a child under the age of 14 has provided us with personal data, we will delete the information from our records.
-## Questions about data processing by us
+## Changes to this Privacy Policy
-If you have any questions about our data processing, please email us or contact the person in our organization listed at the beginning of this privacy statement directly.
+We may revise this privacy policy from time to time and will post the date it was last updated at the top of this privacy policy. We will provide additional notice to you if we make any changes that materially affect your privacy rights.
+
+## Contact us
+
+If you have any questions about our data processing, please email us or contact us by using the contact details listed at the beginning of this privacy notice.
diff --git a/docs/src/components/pii_table.jsx b/docs/src/components/pii_table.jsx
new file mode 100644
index 0000000000..5075e1d2ca
--- /dev/null
+++ b/docs/src/components/pii_table.jsx
@@ -0,0 +1,106 @@
+import React from "react";
+
+export function PiiTable() {
+
+ const pii = [
+ {
+ type: "Basic data",
+ examples: [
+ 'Names',
+ 'Email addresses',
+ 'User names'
+ ],
+ subjects: "All users as uploaded by Customer."
+ },
+ {
+ type: "Login data",
+ examples: [
+ 'Randomly generated ID',
+ 'Passwords',
+ 'Public keys / certificates ("FIDO2", "U2F", "x509", ...)',
+ 'User names or identifiers of external login providers',
+ 'Phone numbers',
+ ],
+ subjects: "All users as uploaded and feature use by Customer."
+ },
+ {
+ type: "Profile data",
+ examples: [
+ 'Profile pictures',
+ 'Gender',
+ 'Languages',
+ 'Nicknames or Display names',
+ 'Phone numbers',
+ 'Metadata'
+ ],
+ subjects: "All users as uploaded by Customer"
+ },
+ {
+ type: "Communication data",
+ examples: [
+ 'Emails',
+ 'Chats',
+ 'Call metadata',
+ 'Call recording and transcripts',
+ 'Form submissions',
+ ],
+ subjects: "Customers and users who communicate with us directly (e.g. support, chat)."
+ },
+ {
+ type: "Payment data",
+ examples: [
+ 'Billing address',
+ 'Payment information',
+ 'Customer number',
+ 'Support Customer history',
+ 'Credit rating information',
+ ],
+ subjects: "Customers who use services that require payment. Credit rating information: Only customers who pay by invoice."
+ },
+ {
+ type: "Analytics data",
+ examples: [
+ 'Usage metrics',
+ 'User behavior',
+ 'User journeys (eg, Milestones)',
+ 'Telemetry data',
+ 'Client-side anonymized session replay',
+ ],
+ subjects: "Customers who use our services."
+ },
+ {
+ type: "Usage meta data",
+ examples: [
+ 'User agent',
+ 'IP addresses',
+ 'Operating system',
+ 'Time and date',
+ 'URL',
+ 'Referrer URL',
+ 'Accepted Language',
+ ],
+ subjects: "All users"
+ },
+ ]
+
+ return (
+
+
+ Type of personal data
+ Examples
+ Affected data subjects
+
+ {
+ pii.map((row, rowID) => {
+ return (
+
+ {row.type}
+ {row.examples.map((example) => { return ( {example} )})}
+ {row.subjects}
+
+ )
+ })
+ }
+
+ );
+}
diff --git a/docs/src/components/subprocessors.jsx b/docs/src/components/subprocessors.jsx
deleted file mode 100644
index a6bf10eee8..0000000000
--- a/docs/src/components/subprocessors.jsx
+++ /dev/null
@@ -1,162 +0,0 @@
-import React from "react";
-
-export function SubProcessorTable() {
-
- const country_list = {
- us: "USA",
- eu: "EU",
- ch: "Switzerland",
- fr: "France",
- in: "India",
- de: "Germany",
- ee: "Estonia",
- nl: "Netherlands",
- ro: "Romania",
- }
- const processors = [
- {
- entity: "Google LLC",
- purpose: "Cloud infrastructure provider (Google Cloud), business applications and collaboration (Workspace), Data warehouse services, Content delivery network, DDoS and bot prevention",
- hosting: "Region designated by Customer, United States",
- country: country_list.us,
- enduserdata: "Yes"
- },
- {
- entity: "Datadog, Inc.",
- purpose: "Infrastructure monitoring, log analytics, and alerting",
- hosting: country_list.eu,
- country: country_list.us,
- enduserdata: "Yes (logs)"
- },
- {
- entity: "Github, Inc.",
- purpose: "Source code management, code scanning, dependency management, security advisory, issue management, continuous integration",
- hosting: country_list.us,
- country: country_list.us,
- enduserdata: false
- },
- {
- entity: "Stripe Payments Europe, Ltd.",
- purpose: "Subscription management, payment process",
- hosting: country_list.us,
- country: country_list.us,
- enduserdata: false
- },
- {
- entity: "Bexio AG",
- purpose: "Customer management, payment process",
- hosting: country_list.ch,
- country: country_list.ch,
- enduserdata: false
- },
- {
- entity: "Mailjet SAS",
- purpose: "Marketing automation",
- hosting: country_list.eu,
- country: country_list.fr,
- enduserdata: false
- },
- {
- entity: "Postmark (AC PM LLC)",
- purpose: "Transactional mails, if no customer owned SMTP service is configured",
- hosting: country_list.us,
- country: country_list.us,
- enduserdata: "Yes (opt-out)"
- },
- {
- entity: "Vercel, Inc.",
- purpose: "Website hosting",
- hosting: country_list.us,
- country: country_list.us,
- enduserdata: false
- },
- {
- entity: "Agolia SAS",
- purpose: "Documentation search engine (zitadel.com/docs)",
- hosting: country_list.us,
- country: country_list.in,
- enduserdata: false
- },
- {
- entity: "Discord Netherlands BV",
- purpose: "Community chat (zitadel.com/chat)",
- hosting: country_list.us,
- country: country_list.us,
- enduserdata: false
- },
- {
- entity: "Statuspal",
- purpose: "ZITADEL Cloud service status announcements",
- hosting: country_list.us,
- country: country_list.de,
- enduserdata: false
- },
- {
- entity: "Plausible Insights OÜ",
- purpose: "Privacy-friendly web analytics",
- hosting: country_list.de,
- country: country_list.ee,
- enduserdata: false,
- dpa: 'https://plausible.io/dpa'
- },
- {
- entity: "Twillio Inc.",
- purpose: "Messaging platform for SMS",
- hosting: country_list.us,
- country: country_list.us,
- enduserdata: "Yes (opt-out)"
- },
- {
- entity: "Mohlmann Solutions SRL",
- purpose: "Global payroll",
- hosting: undefined,
- country: country_list.ro,
- enduserdata: false
- },
- {
- entity: "Remote Europe Holding, B.V.",
- purpose: "Global payroll",
- hosting: undefined,
- country: country_list.nl,
- enduserdata: false
- },
- {
- entity: "HubSpot Inc.",
- purpose: "Customer and sales management, Marketing automation, Support requests",
- hosting: country_list.eu,
- country: country_list.us,
- enduserdata: false
- },
- ]
-
- return (
-
-
- Entity name
- Purpose
- End-user data
- Hosting location
- Country of registration
-
- {
- processors
- .sort((a, b) => {
- if (a.entity < b.entity) return -1
- if (a.entity > b.entity) return 1
- else return 0
- })
- .map((processor, rowID) => {
- return (
-
- {processor.entity}
- {processor.purpose}
- {processor.enduserdata ? processor.enduserdata : 'No'}
- {processor.hosting ? processor.hosting : 'n/a'}
- {processor.country}
-
- )
- })
- }
-
- );
-}
From d71795c43354ec6ba6134cf0b06ef0ba951b86d0 Mon Sep 17 00:00:00 2001
From: Livio Spring
Date: Thu, 8 May 2025 08:35:34 +0200
Subject: [PATCH 045/181] fix: remove index es_instance_position (#9862)
# Which Problems Are Solved
#9837 added a new index `es_instance_position` on the events table with
the idea to improve performance for some projections. Unfortunately, it
makes it worse for almost all projections and would only improve the
situation for the events handler of the actions V2 subscriptions.
# How the Problems Are Solved
Remove the index again.
# Additional Changes
None
# Additional Context
relates to #9837
relates to #9863
---
cmd/setup/54.go | 2 +-
cmd/setup/54.sql | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/cmd/setup/54.go b/cmd/setup/54.go
index 3dd2f60abe..9d65264941 100644
--- a/cmd/setup/54.go
+++ b/cmd/setup/54.go
@@ -23,5 +23,5 @@ func (mig *InstancePositionIndex) Execute(ctx context.Context, _ eventstore.Even
}
func (mig *InstancePositionIndex) String() string {
- return "54_instance_position_index"
+ return "54_instance_position_index_remove"
}
diff --git a/cmd/setup/54.sql b/cmd/setup/54.sql
index 1dca8c7575..927bd2aa9b 100644
--- a/cmd/setup/54.sql
+++ b/cmd/setup/54.sql
@@ -1 +1 @@
-CREATE INDEX CONCURRENTLY IF NOT EXISTS es_instance_position ON eventstore.events2 (instance_id, position);
+DROP INDEX IF EXISTS eventstore.es_instance_position;
From 867e9cb15a92786a5953a84beefcb85e296e80ba Mon Sep 17 00:00:00 2001
From: Livio Spring
Date: Thu, 8 May 2025 09:32:41 +0200
Subject: [PATCH 046/181] fix: correctly use single matching user (by
loginname) (#9865)
# Which Problems Are Solved
In rare cases there was a possibility that multiple users were found by
a loginname. This prevented the corresponding user to sign in.
# How the Problems Are Solved
Fixed the corresponding query (to correctly respect the org domain
policy).
# Additional Changes
None
# Additional Context
Found during the investigation of a support request
---
internal/query/user_by_login_name.sql | 2 +-
internal/query/user_notify_by_login_name.sql | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/internal/query/user_by_login_name.sql b/internal/query/user_by_login_name.sql
index a37c213612..f7d2fd3e23 100644
--- a/internal/query/user_by_login_name.sql
+++ b/internal/query/user_by_login_name.sql
@@ -10,7 +10,7 @@ WITH found_users AS (
LEFT JOIN projections.login_names3_policies p_custom
ON u.instance_id = p_custom.instance_id
AND p_custom.instance_id = $4 AND p_custom.resource_owner = u.resource_owner
- LEFT JOIN projections.login_names3_policies p_default
+ JOIN projections.login_names3_policies p_default
ON u.instance_id = p_default.instance_id
AND p_default.instance_id = $4 AND p_default.is_default IS TRUE
AND (
diff --git a/internal/query/user_notify_by_login_name.sql b/internal/query/user_notify_by_login_name.sql
index 5b23cd61a7..090a8991f9 100644
--- a/internal/query/user_notify_by_login_name.sql
+++ b/internal/query/user_notify_by_login_name.sql
@@ -10,7 +10,7 @@ WITH found_users AS (
LEFT JOIN projections.login_names3_policies p_custom
ON u.instance_id = p_custom.instance_id
AND p_custom.instance_id = $4 AND p_custom.resource_owner = u.resource_owner
- LEFT JOIN projections.login_names3_policies p_default
+ JOIN projections.login_names3_policies p_default
ON u.instance_id = p_default.instance_id
AND p_default.instance_id = $4 AND p_default.is_default IS TRUE
AND (
From 60ce32ca4fbd818a64524294653923e0ffa81688 Mon Sep 17 00:00:00 2001
From: Silvan <27845747+adlerhurst@users.noreply.github.com>
Date: Thu, 8 May 2025 17:13:57 +0200
Subject: [PATCH 047/181] fix(setup): reenable index creation (#9868)
# Which Problems Are Solved
We saw high CPU usage if many events were created on the database. This
was caused by the new actions which query for all event types and
aggregate types.
# How the Problems Are Solved
- the handler of action execution does not filter for aggregate and
event types.
- the index for `instance_id` and `position` is reenabled.
# Additional Changes
none
# Additional Context
none
---
cmd/setup/54.go | 2 +-
cmd/setup/54.sql | 2 +-
internal/eventstore/handler/v2/handler.go | 14 ++++++++++++++
internal/execution/handlers.go | 3 +++
4 files changed, 19 insertions(+), 2 deletions(-)
diff --git a/cmd/setup/54.go b/cmd/setup/54.go
index 9d65264941..e4a2e43862 100644
--- a/cmd/setup/54.go
+++ b/cmd/setup/54.go
@@ -23,5 +23,5 @@ func (mig *InstancePositionIndex) Execute(ctx context.Context, _ eventstore.Even
}
func (mig *InstancePositionIndex) String() string {
- return "54_instance_position_index_remove"
+ return "54_instance_position_index_again"
}
diff --git a/cmd/setup/54.sql b/cmd/setup/54.sql
index 927bd2aa9b..1dca8c7575 100644
--- a/cmd/setup/54.sql
+++ b/cmd/setup/54.sql
@@ -1 +1 @@
-DROP INDEX IF EXISTS eventstore.es_instance_position;
+CREATE INDEX CONCURRENTLY IF NOT EXISTS es_instance_position ON eventstore.events2 (instance_id, position);
diff --git a/internal/eventstore/handler/v2/handler.go b/internal/eventstore/handler/v2/handler.go
index 43c3e58b3b..fb696ad090 100644
--- a/internal/eventstore/handler/v2/handler.go
+++ b/internal/eventstore/handler/v2/handler.go
@@ -60,6 +60,7 @@ type Handler struct {
requeueEvery time.Duration
txDuration time.Duration
now nowFunc
+ queryGlobal bool
triggeredInstancesSync sync.Map
@@ -143,6 +144,11 @@ type Projection interface {
Reducers() []AggregateReducer
}
+type GlobalProjection interface {
+ Projection
+ FilterGlobalEvents()
+}
+
func NewHandler(
ctx context.Context,
config *Config,
@@ -185,6 +191,10 @@ func NewHandler(
metrics: metrics,
}
+ if _, ok := projection.(GlobalProjection); ok {
+ handler.queryGlobal = true
+ }
+
return handler
}
@@ -676,6 +686,10 @@ func (h *Handler) eventQuery(currentState *state) *eventstore.SearchQueryBuilder
}
}
+ if h.queryGlobal {
+ return builder
+ }
+
aggregateTypes := make([]eventstore.AggregateType, 0, len(h.eventTypes))
eventTypes := make([]eventstore.EventType, 0, len(h.eventTypes))
diff --git a/internal/execution/handlers.go b/internal/execution/handlers.go
index 7ffb4cc6ff..030e6d5186 100644
--- a/internal/execution/handlers.go
+++ b/internal/execution/handlers.go
@@ -84,6 +84,9 @@ func (u *eventHandler) Reducers() []handler.AggregateReducer {
return aggReducers
}
+// FilterGlobalEvents implements [handler.GlobalProjection]
+func (u *eventHandler) FilterGlobalEvents() {}
+
func groupsFromEventType(s string) []string {
parts := strings.Split(s, ".")
groups := make([]string, len(parts))
From 28856015d6116d1989c9c3d95e85d10df7068c2a Mon Sep 17 00:00:00 2001
From: subaru <79771445+subaru-hello@users.noreply.github.com>
Date: Mon, 12 May 2025 17:04:32 +0900
Subject: [PATCH 048/181] feat(console): Add organization ID filter to
organization list (#9823)
# Which Problems Are Solved
Replace this example text with a concise list of problems that this PR
solves.
- Organization list lacked the ability to filter by organization ID
- No efficient method was provided for users to search organizations by
ID
# How the Problems Are Solved
Replace this example text with a concise list of changes that this PR
introduces.
- Added organization ID filtering functionality to
`filter-org.component.ts`
- Added `ID` to the `SubQuery` enum
- Added `ID` case handling to `changeCheckbox`, `setValue`, and
`getSubFilter` methods
- Added ID filter UI to `filter-org.component.html`
- Added checkbox and text input field
- Used translation key to display "Organization ID" label
- Added new translation key to translation file (`en.json`)
- Added `FILTER.ORGID` key with "Organization ID" value
# Additional Changes
Replace this example text with a concise list of additional changes that
this PR introduces, that are not directly solving the initial problem
but are related.
- Maintained consistency with existing filtering functionality
- Ensured intuitive user interface usability
- Added new key while maintaining translation file structure
# Additional Context
Replace this example with links to related issues, discussions, discord
threads, or other sources with more context.
Use the Closing #issue syntax for issues that are resolved with this PR.
- Closes #8792
- Discussion #xxx
- Follow-up for PR #xxx
- https://discord.com/channels/xxx/xxx
---------
Co-authored-by: Marco A.
---
.../filter-org/filter-org.component.html | 15 +++++++
.../filter-org/filter-org.component.ts | 40 ++++++++++++++++++-
console/src/assets/i18n/bg.json | 2 +
console/src/assets/i18n/cs.json | 2 +
console/src/assets/i18n/de.json | 2 +
console/src/assets/i18n/en.json | 8 ++--
console/src/assets/i18n/es.json | 2 +
console/src/assets/i18n/fr.json | 2 +
console/src/assets/i18n/hu.json | 2 +
console/src/assets/i18n/id.json | 2 +
console/src/assets/i18n/it.json | 2 +
console/src/assets/i18n/ja.json | 2 +
console/src/assets/i18n/ko.json | 2 +
console/src/assets/i18n/mk.json | 2 +
console/src/assets/i18n/nl.json | 2 +
console/src/assets/i18n/pl.json | 2 +
console/src/assets/i18n/pt.json | 2 +
console/src/assets/i18n/ro.json | 2 +
console/src/assets/i18n/ru.json | 2 +
console/src/assets/i18n/sv.json | 2 +
console/src/assets/i18n/zh.json | 2 +
21 files changed, 95 insertions(+), 4 deletions(-)
diff --git a/console/src/app/modules/filter-org/filter-org.component.html b/console/src/app/modules/filter-org/filter-org.component.html
index 4e8535fe76..ae42667f49 100644
--- a/console/src/app/modules/filter-org/filter-org.component.html
+++ b/console/src/app/modules/filter-org/filter-org.component.html
@@ -69,4 +69,19 @@
+
+
+
{{ 'FILTER.ORGID' | translate }}
+
+
+
+ {{ 'FILTER.METHODS.1' | translate }}
+
+
+
+
+
+
+
diff --git a/console/src/app/modules/filter-org/filter-org.component.ts b/console/src/app/modules/filter-org/filter-org.component.ts
index 220b219358..4ea9a6ea6e 100644
--- a/console/src/app/modules/filter-org/filter-org.component.ts
+++ b/console/src/app/modules/filter-org/filter-org.component.ts
@@ -3,7 +3,14 @@ import { MatCheckboxChange } from '@angular/material/checkbox';
import { ActivatedRoute, Router } from '@angular/router';
import { take } from 'rxjs';
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
-import { OrgDomainQuery, OrgNameQuery, OrgQuery, OrgState, OrgStateQuery } from 'src/app/proto/generated/zitadel/org_pb';
+import {
+ OrgDomainQuery,
+ OrgNameQuery,
+ OrgQuery,
+ OrgState,
+ OrgStateQuery,
+ OrgIDQuery,
+} from 'src/app/proto/generated/zitadel/org_pb';
import { UserNameQuery } from 'src/app/proto/generated/zitadel/user_pb';
import { FilterComponent } from '../filter/filter.component';
@@ -12,6 +19,7 @@ enum SubQuery {
NAME,
STATE,
DOMAIN,
+ ID,
}
@Component({
@@ -61,6 +69,12 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
orgDomainQuery.setMethod(filter.domainQuery.method);
orgQuery.setDomainQuery(orgDomainQuery);
return orgQuery;
+ } else if (filter.idQuery) {
+ const orgQuery = new OrgQuery();
+ const orgIdQuery = new OrgIDQuery();
+ orgIdQuery.setId(filter.idQuery.id);
+ orgQuery.setIdQuery(orgIdQuery);
+ return orgQuery;
} else {
return undefined;
}
@@ -100,6 +114,13 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
odq.setDomainQuery(dq);
this.searchQueries.push(odq);
break;
+ case SubQuery.ID:
+ const idq = new OrgIDQuery();
+ idq.setId('');
+ const oidq = new OrgQuery();
+ oidq.setIdQuery(idq);
+ this.searchQueries.push(oidq);
+ break;
}
} else {
switch (subquery) {
@@ -121,6 +142,12 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
this.searchQueries.splice(index_pdn, 1);
}
break;
+ case SubQuery.ID:
+ const index_id = this.searchQueries.findIndex((q) => (q as OrgQuery).toObject().idQuery !== undefined);
+ if (index_id > -1) {
+ this.searchQueries.splice(index_id, 1);
+ }
+ break;
}
}
}
@@ -140,6 +167,10 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
(query as OrgDomainQuery).setDomain(value);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break;
+ case SubQuery.ID:
+ (query as OrgIDQuery).setId(value);
+ this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
+ break;
}
}
@@ -166,6 +197,13 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
} else {
return undefined;
}
+ case SubQuery.ID:
+ const id = this.searchQueries.find((q) => (q as OrgQuery).toObject().idQuery !== undefined);
+ if (id) {
+ return (id as OrgQuery).getIdQuery();
+ } else {
+ return undefined;
+ }
}
}
diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json
index b98204a917..b56f5797ec 100644
--- a/console/src/assets/i18n/bg.json
+++ b/console/src/assets/i18n/bg.json
@@ -692,6 +692,7 @@
"EMAIL": "електронна поща",
"USERNAME": "Потребителско име",
"ORGNAME": "Наименование на организацията",
+ "ORGID": "Идентификатор на организацията",
"PRIMARYDOMAIN": "Основен домейн",
"PROJECTNAME": "Име на проекта",
"RESOURCEOWNER": "Собственик на ресурс",
@@ -2150,6 +2151,7 @@
"ACTIVATE": "Активиране на проекта",
"DELETE": "Изтриване на проекта",
"ORGNAME": "Наименование на организацията",
+ "ORGID": "Идентификатор на организацията",
"ORGDOMAIN": "Домейн на организацията",
"STATE": "Статус",
"TYPE": "Тип",
diff --git a/console/src/assets/i18n/cs.json b/console/src/assets/i18n/cs.json
index 390c5dcdbd..bf977aab43 100644
--- a/console/src/assets/i18n/cs.json
+++ b/console/src/assets/i18n/cs.json
@@ -693,6 +693,7 @@
"EMAIL": "Email",
"USERNAME": "Uživatelské jméno",
"ORGNAME": "Název organizace",
+ "ORGID": "ID organizace",
"PRIMARYDOMAIN": "Primární doména",
"PROJECTNAME": "Název projektu",
"RESOURCEOWNER": "Vlastník zdroje",
@@ -2151,6 +2152,7 @@
"ACTIVATE": "Aktivovat projekt",
"DELETE": "Smazat projekt",
"ORGNAME": "Název organizace",
+ "ORGID": "ID organizace",
"ORGDOMAIN": "Doména organizace",
"STATE": "Stav",
"TYPE": "Typ",
diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json
index e73c883bd2..12a2f56792 100644
--- a/console/src/assets/i18n/de.json
+++ b/console/src/assets/i18n/de.json
@@ -693,6 +693,7 @@
"EMAIL": "Email",
"USERNAME": "Nutzername",
"ORGNAME": "Organisationsname",
+ "ORGID": "Organisations ID",
"PRIMARYDOMAIN": "Primäre Domäne",
"PROJECTNAME": "Projektname",
"RESOURCEOWNER": "Ressourcenbesitzer",
@@ -2150,6 +2151,7 @@
"ACTIVATE": "Projekt aktivieren",
"DELETE": "Projekt löschen",
"ORGNAME": "Name der Organisation",
+ "ORGID": "Organisations ID",
"ORGDOMAIN": "Domain der Organisation",
"STATE": "Status",
"TYPE": "Typ",
diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json
index 5e2cc3f4c9..571a2bd577 100644
--- a/console/src/assets/i18n/en.json
+++ b/console/src/assets/i18n/en.json
@@ -688,12 +688,13 @@
},
"FILTER": {
"TITLE": "Filter",
- "STATE": "Status",
+ "ORGNAME": "Organization Name",
+ "ORGID": "Organization ID",
+ "STATE": "State",
+ "PRIMARYDOMAIN": "Primary Domain",
"DISPLAYNAME": "User Display Name",
"EMAIL": "Email",
"USERNAME": "User Name",
- "ORGNAME": "Organization Name",
- "PRIMARYDOMAIN": "Primary Domain",
"PROJECTNAME": "Project Name",
"RESOURCEOWNER": "Resource Owner",
"METHODS": {
@@ -2153,6 +2154,7 @@
"ACTIVATE": "Activate Project",
"DELETE": "Delete Project",
"ORGNAME": "Organization Name",
+ "ORGID": "Organization ID",
"ORGDOMAIN": "Organization Domain",
"STATE": "Status",
"TYPE": "Type",
diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json
index 198bb3ca8b..19bf21238f 100644
--- a/console/src/assets/i18n/es.json
+++ b/console/src/assets/i18n/es.json
@@ -693,6 +693,7 @@
"EMAIL": "Email",
"USERNAME": "Nombre de usuario",
"ORGNAME": "Nombre de organización",
+ "ORGID": "ID de organización",
"PRIMARYDOMAIN": "Dominio primario",
"PROJECTNAME": "Nombre de proyecto",
"RESOURCEOWNER": "Propietario del recurso",
@@ -2151,6 +2152,7 @@
"ACTIVATE": "Activar proyecto",
"DELETE": "Borrar proyecto",
"ORGNAME": "Nombre de organización",
+ "ORGID": "ID de organización",
"ORGDOMAIN": "Dominio de organización",
"STATE": "Estado",
"TYPE": "Tipo",
diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json
index 0d66c4193e..1203a09262 100644
--- a/console/src/assets/i18n/fr.json
+++ b/console/src/assets/i18n/fr.json
@@ -693,6 +693,7 @@
"EMAIL": "Courriel",
"USERNAME": "Nom de l'utilisateur",
"ORGNAME": "Nom de l'organisation",
+ "ORGID": "ID de l'organisation",
"PRIMARYDOMAIN": "Domaine principal",
"PROJECTNAME": "Nom du projet",
"RESOURCEOWNER": "Propriétaire des ressources",
@@ -2150,6 +2151,7 @@
"ACTIVATE": "Activer le projet",
"DELETE": "Supprimer le projet",
"ORGNAME": "Nom de l'organisation",
+ "ORGID": "ID de l'organisation",
"ORGDOMAIN": "Domaine de l'organisation",
"STATE": "Statut",
"TYPE": "Type",
diff --git a/console/src/assets/i18n/hu.json b/console/src/assets/i18n/hu.json
index 96d1fe16df..eaf8b5f503 100644
--- a/console/src/assets/i18n/hu.json
+++ b/console/src/assets/i18n/hu.json
@@ -693,6 +693,7 @@
"EMAIL": "E-mail",
"USERNAME": "Felhasználói Név",
"ORGNAME": "Szervezet Neve",
+ "ORGID": "Szervezet ID",
"PRIMARYDOMAIN": "Elsődleges Domain",
"PROJECTNAME": "Projekt Neve",
"RESOURCEOWNER": "Erőforrás Tulajdonos",
@@ -2148,6 +2149,7 @@
"ACTIVATE": "Projekt aktiválása",
"DELETE": "Projekt törlése",
"ORGNAME": "Szervezet neve",
+ "ORGID": "Szervezet ID",
"ORGDOMAIN": "Szervezet domainje",
"STATE": "Státusz",
"TYPE": "Típus",
diff --git a/console/src/assets/i18n/id.json b/console/src/assets/i18n/id.json
index ca788a9467..e6c8e8ca3d 100644
--- a/console/src/assets/i18n/id.json
+++ b/console/src/assets/i18n/id.json
@@ -652,6 +652,7 @@
"EMAIL": "E-mail",
"USERNAME": "Nama belakang",
"ORGNAME": "Nama Organisasi",
+ "ORGID": "ID Organisasi",
"PRIMARYDOMAIN": "Domain Utama",
"PROJECTNAME": "Nama Proyek",
"RESOURCEOWNER": "Pemilik Sumber Daya",
@@ -1980,6 +1981,7 @@
"ACTIVATE": "Aktifkan Proyek",
"DELETE": "Hapus Proyek",
"ORGNAME": "Nama Organisasi",
+ "ORGID": "ID Organisasi",
"ORGDOMAIN": "Domain Organisasi",
"STATE": "Status",
"TYPE": "Jenis",
diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json
index 60266bdac5..3609c2b8f2 100644
--- a/console/src/assets/i18n/it.json
+++ b/console/src/assets/i18n/it.json
@@ -692,6 +692,7 @@
"EMAIL": "Email",
"USERNAME": "User Name",
"ORGNAME": "Nome organizzazione",
+ "ORGID": "ID organizzazione",
"PRIMARYDOMAIN": "Dominio primario",
"PROJECTNAME": "Nome del progetto",
"RESOURCEOWNER": "Resource Owner",
@@ -2150,6 +2151,7 @@
"ACTIVATE": "Attiva progetto",
"DELETE": "Rimuovi progetto",
"ORGNAME": "Nome dell'organizzazione",
+ "ORGID": "ID organizzazione",
"ORGDOMAIN": "Dominio",
"STATE": "Stato",
"TYPE": "Tipo",
diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json
index 288d491ce7..2d91632ad1 100644
--- a/console/src/assets/i18n/ja.json
+++ b/console/src/assets/i18n/ja.json
@@ -693,6 +693,7 @@
"EMAIL": "Eメール",
"USERNAME": "ユーザー名",
"ORGNAME": "組織名",
+ "ORGID": "組織ID",
"PRIMARYDOMAIN": "プライマリドメイン",
"PROJECTNAME": "プロジェクト名",
"RESOURCEOWNER": "リソース所有者",
@@ -2150,6 +2151,7 @@
"ACTIVATE": "プロジェクトのアクティブ化",
"DELETE": "プロジェクトの削除",
"ORGNAME": "組織名",
+ "ORGID": "組織ID",
"ORGDOMAIN": "組織ドメイン",
"STATE": "ステータス",
"TYPE": "タイプ",
diff --git a/console/src/assets/i18n/ko.json b/console/src/assets/i18n/ko.json
index 437c43a3a1..8137ed98df 100644
--- a/console/src/assets/i18n/ko.json
+++ b/console/src/assets/i18n/ko.json
@@ -693,6 +693,7 @@
"EMAIL": "이메일",
"USERNAME": "사용자 이름",
"ORGNAME": "조직 이름",
+ "ORGID": "조직 ID",
"PRIMARYDOMAIN": "기본 도메인",
"PROJECTNAME": "프로젝트 이름",
"RESOURCEOWNER": "리소스 소유자",
@@ -2150,6 +2151,7 @@
"ACTIVATE": "프로젝트 활성화",
"DELETE": "프로젝트 삭제",
"ORGNAME": "조직 이름",
+ "ORGID": "조직 ID",
"ORGDOMAIN": "조직 도메인",
"STATE": "상태",
"TYPE": "유형",
diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json
index 2e62723939..f11bcc5d63 100644
--- a/console/src/assets/i18n/mk.json
+++ b/console/src/assets/i18n/mk.json
@@ -693,6 +693,7 @@
"EMAIL": "Е-пошта",
"USERNAME": "Корисничко име",
"ORGNAME": "Име на организацијата",
+ "ORGID": "Идентификатор на организацијата",
"PRIMARYDOMAIN": "Примарен домен",
"PROJECTNAME": "Име на проектот",
"RESOURCEOWNER": "Сопственик на ресурсот",
@@ -2153,6 +2154,7 @@
"ACTIVATE": "Активирај проект",
"DELETE": "Избриши проект",
"ORGNAME": "Име на организација",
+ "ORGID": "Идентификатор на организација",
"ORGDOMAIN": "Домен на организација",
"STATE": "Статус",
"TYPE": "Тип",
diff --git a/console/src/assets/i18n/nl.json b/console/src/assets/i18n/nl.json
index 7e549f64ba..54fb6dfb26 100644
--- a/console/src/assets/i18n/nl.json
+++ b/console/src/assets/i18n/nl.json
@@ -693,6 +693,7 @@
"EMAIL": "E-mail",
"USERNAME": "Gebruikersnaam",
"ORGNAME": "Organisatienaam",
+ "ORGID": "Organisatie ID",
"PRIMARYDOMAIN": "Primair domein",
"PROJECTNAME": "Projectnaam",
"RESOURCEOWNER": "Eigenaar van de bron",
@@ -2150,6 +2151,7 @@
"ACTIVATE": "Activeer Project",
"DELETE": "Verwijder Project",
"ORGNAME": "Organisatienaam",
+ "ORGID": "Organisatie ID",
"ORGDOMAIN": "Organisatie Domein",
"STATE": "Status",
"TYPE": "Type",
diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json
index 2f18c343f7..ae23c79427 100644
--- a/console/src/assets/i18n/pl.json
+++ b/console/src/assets/i18n/pl.json
@@ -692,6 +692,7 @@
"EMAIL": "Email",
"USERNAME": "Nazwa Użytkownika",
"ORGNAME": "Nazwa Organizacji",
+ "ORGID": "ID Organizacji",
"PRIMARYDOMAIN": "Domena podstawowa",
"PROJECTNAME": "Nazwa Projektu",
"RESOURCEOWNER": "Właściciel Zasobu",
@@ -2149,6 +2150,7 @@
"ACTIVATE": "Aktywuj projekt",
"DELETE": "Usuń projekt",
"ORGNAME": "Nazwa organizacji",
+ "ORGID": "ID organizacji",
"ORGDOMAIN": "Domena organizacji",
"STATE": "Status",
"TYPE": "Typ",
diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json
index 08181f6ead..71abb8d51f 100644
--- a/console/src/assets/i18n/pt.json
+++ b/console/src/assets/i18n/pt.json
@@ -693,6 +693,7 @@
"EMAIL": "E-mail",
"USERNAME": "Nome de Usuário",
"ORGNAME": "Nome da Organização",
+ "ORGID": "ID da Organização",
"PRIMARYDOMAIN": "Domínio primário",
"PROJECTNAME": "Nome do Projeto",
"RESOURCEOWNER": "Proprietário do Recurso",
@@ -2152,6 +2153,7 @@
"ACTIVATE": "Ativar Projeto",
"DELETE": "Excluir Projeto",
"ORGNAME": "Nome da Organização",
+ "ORGID": "ID da Organização",
"ORGDOMAIN": "Domínio da Organização",
"STATE": "Status",
"TYPE": "Tipo",
diff --git a/console/src/assets/i18n/ro.json b/console/src/assets/i18n/ro.json
index b07897f316..f6a183bede 100644
--- a/console/src/assets/i18n/ro.json
+++ b/console/src/assets/i18n/ro.json
@@ -691,6 +691,7 @@
"EMAIL": "E-mail",
"USERNAME": "Numele utilizatorului",
"ORGNAME": "Numele organizației",
+ "ORGID": "ID-ul organizației",
"PRIMARYDOMAIN": "Domeniu principal",
"PROJECTNAME": "Numele proiectului",
"RESOURCEOWNER": "Proprietarul resursei",
@@ -2148,6 +2149,7 @@
"ACTIVATE": "Activați proiectul",
"DELETE": "Ștergeți proiectul",
"ORGNAME": "Nume organizație",
+ "ORGID": "ID organizație",
"ORGDOMAIN": "Domeniu organizație",
"STATE": "Stare",
"TYPE": "Tip",
diff --git a/console/src/assets/i18n/ru.json b/console/src/assets/i18n/ru.json
index c6ef31499e..c4955f83fd 100644
--- a/console/src/assets/i18n/ru.json
+++ b/console/src/assets/i18n/ru.json
@@ -693,6 +693,7 @@
"EMAIL": "Электронная почта",
"USERNAME": "Имя пользователя",
"ORGNAME": "Название организации",
+ "ORGID": "ID организации",
"PRIMARYDOMAIN": "Основной домен",
"PROJECTNAME": "Название проекта",
"RESOURCEOWNER": "Владелец ресурса",
@@ -2237,6 +2238,7 @@
"ACTIVATE": "Активировать проект",
"DELETE": "Удалить проект",
"ORGNAME": "Название организации",
+ "ORGID": "ID организации",
"ORGDOMAIN": "Домен организации",
"STATE": "Статус",
"TYPE": "Тип",
diff --git a/console/src/assets/i18n/sv.json b/console/src/assets/i18n/sv.json
index c356e635e0..1144fd4585 100644
--- a/console/src/assets/i18n/sv.json
+++ b/console/src/assets/i18n/sv.json
@@ -693,6 +693,7 @@
"EMAIL": "E-post",
"USERNAME": "Användarnamn",
"ORGNAME": "Organisationsnamn",
+ "ORGID": "Organisations ID",
"PRIMARYDOMAIN": "Primär domän",
"PROJECTNAME": "Projektnamn",
"RESOURCEOWNER": "Resursägare",
@@ -2154,6 +2155,7 @@
"ACTIVATE": "Aktivera projekt",
"DELETE": "Ta bort projekt",
"ORGNAME": "Organisationsnamn",
+ "ORGID": "Organisations ID",
"ORGDOMAIN": "Organisationsdomän",
"STATE": "Status",
"TYPE": "Typ",
diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json
index 8be3316b0b..943a9aef16 100644
--- a/console/src/assets/i18n/zh.json
+++ b/console/src/assets/i18n/zh.json
@@ -693,6 +693,7 @@
"EMAIL": "邮箱",
"USERNAME": "用户名",
"ORGNAME": "组织名称",
+ "ORGID": "组织ID",
"PRIMARYDOMAIN": "主域",
"PROJECTNAME": "项目名称",
"RESOURCEOWNER": "资源所有者",
@@ -2149,6 +2150,7 @@
"ACTIVATE": "启用项目",
"DELETE": "删除项目",
"ORGNAME": "组织名称",
+ "ORGID": "组织ID",
"ORGDOMAIN": "组织域名",
"STATE": "状态",
"TYPE": "类型",
From d79d5e7b964e3f1592489e956156f8f1c87e4710 Mon Sep 17 00:00:00 2001
From: Elio Bischof
Date: Mon, 12 May 2025 12:05:12 +0200
Subject: [PATCH 049/181] fix(projection): remove users with factors (#9877)
# Which Problems Are Solved
When users are removed, their auth factors stay in the projection. This
data inconsistency is visible if a removed user is recreated with the
same ID. In such a case, the login UI and the query API methods show the
removed users auth methods. This is unexpected behavior.
The old users auth methods are not usable to log in and they are not
found by the command side. This is expected behavior.
# How the Problems Are Solved
The auth factors projection reduces the user removed event by deleting
all factors.
# Additional Context
- Reported by support request
- requires backport to 2.x and 3.x
---
internal/query/projection/user_auth_method.go | 19 +++++++++++++
.../query/projection/user_auth_method_test.go | 28 +++++++++++++++++++
2 files changed, 47 insertions(+)
diff --git a/internal/query/projection/user_auth_method.go b/internal/query/projection/user_auth_method.go
index b986df1558..7726550ffd 100644
--- a/internal/query/projection/user_auth_method.go
+++ b/internal/query/projection/user_auth_method.go
@@ -125,6 +125,10 @@ func (p *userAuthMethodProjection) Reducers() []handler.AggregateReducer {
Event: user.HumanOTPEmailRemovedType,
Reduce: p.reduceRemoveAuthMethod,
},
+ {
+ Event: user.UserRemovedType,
+ Reduce: p.reduceUserRemoved,
+ },
},
},
{
@@ -311,3 +315,18 @@ func (p *userAuthMethodProjection) reduceOwnerRemoved(event eventstore.Event) (*
},
), nil
}
+
+func (p *userAuthMethodProjection) reduceUserRemoved(event eventstore.Event) (*handler.Statement, error) {
+ e, ok := event.(*user.UserRemovedEvent)
+ if !ok {
+ return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-FwDZ8", "reduce.wrong.event.type %s", user.UserRemovedType)
+ }
+ return handler.NewDeleteStatement(
+ e,
+ []handler.Condition{
+ handler.NewCond(UserAuthMethodInstanceIDCol, e.Aggregate().InstanceID),
+ handler.NewCond(UserAuthMethodResourceOwnerCol, e.Aggregate().ResourceOwner),
+ handler.NewCond(UserAuthMethodUserIDCol, e.Aggregate().ID),
+ },
+ ), nil
+}
diff --git a/internal/query/projection/user_auth_method_test.go b/internal/query/projection/user_auth_method_test.go
index fb3d6d9d91..e21a480a9d 100644
--- a/internal/query/projection/user_auth_method_test.go
+++ b/internal/query/projection/user_auth_method_test.go
@@ -528,6 +528,34 @@ func TestUserAuthMethodProjection_reduces(t *testing.T) {
},
},
},
+ {
+ name: "reduceUserRemoved",
+ reduce: (&userAuthMethodProjection{}).reduceUserRemoved,
+ args: args{
+ event: getEvent(
+ testEvent(
+ user.UserRemovedType,
+ user.AggregateType,
+ nil,
+ ), user.UserRemovedEventMapper),
+ },
+ want: wantReduce{
+ aggregateType: eventstore.AggregateType("user"),
+ sequence: 15,
+ executer: &testExecuter{
+ executions: []execution{
+ {
+ expectedStmt: "DELETE FROM projections.user_auth_methods5 WHERE (instance_id = $1) AND (resource_owner = $2) AND (user_id = $3)",
+ expectedArgs: []interface{}{
+ "instance-id",
+ "ro-id",
+ "agg-id",
+ },
+ },
+ },
+ },
+ },
+ },
{
name: "org reduceOwnerRemoved",
reduce: (&userAuthMethodProjection{}).reduceOwnerRemoved,
From 1383cb070264cba0a5ccc5c14762a24ef24a9644 Mon Sep 17 00:00:00 2001
From: Stefan Benz <46600784+stebenz@users.noreply.github.com>
Date: Tue, 13 May 2025 10:32:48 +0200
Subject: [PATCH 050/181] fix: correctly "or"-join ldap userfilters (#9855)
# Which Problems Are Solved
LDAP userfilters are joined, but as it not handled as a list of filters
but as a string they are not or-joined.
# How the Problems Are Solved
Separate userfilters as list of filters and join them correctly with
"or" condition.
# Additional Changes
None
# Additional Context
Closes #7003
---------
Co-authored-by: Marco A.
---
internal/idp/providers/ldap/session.go | 21 ++++++++++++---------
internal/idp/providers/ldap/session_test.go | 10 +++++-----
2 files changed, 17 insertions(+), 14 deletions(-)
diff --git a/internal/idp/providers/ldap/session.go b/internal/idp/providers/ldap/session.go
index 1679e35b61..a78dd02d73 100644
--- a/internal/idp/providers/ldap/session.go
+++ b/internal/idp/providers/ldap/session.go
@@ -133,7 +133,6 @@ func tryBind(
username,
password,
timeout,
- rootCA,
)
}
@@ -189,12 +188,11 @@ func trySearchAndUserBind(
username string,
password string,
timeout time.Duration,
- rootCA []byte,
) (*ldap.Entry, error) {
searchQuery := queriesAndToSearchQuery(
objectClassesToSearchQuery(objectClasses),
queriesOrToSearchQuery(
- userFiltersToSearchQuery(userFilters, username),
+ userFiltersToSearchQuery(userFilters, username)...,
),
)
@@ -218,7 +216,12 @@ func trySearchAndUserBind(
user := sr.Entries[0]
// Bind as the user to verify their password
- if err = conn.Bind(user.DN, password); err != nil {
+ userDN, err := ldap.ParseDN(user.DN)
+ if err != nil {
+ logging.WithFields("userDN", user.DN).WithError(err).Info("ldap user parse DN failed")
+ return nil, err
+ }
+ if err = conn.Bind(userDN.String(), password); err != nil {
logging.WithFields("userDN", user.DN).WithError(err).Info("ldap user bind failed")
return nil, ErrFailedLogin
}
@@ -261,12 +264,12 @@ func objectClassesToSearchQuery(classes []string) string {
return searchQuery
}
-func userFiltersToSearchQuery(filters []string, username string) string {
- searchQuery := ""
- for _, filter := range filters {
- searchQuery += "(" + filter + "=" + ldap.EscapeFilter(username) + ")"
+func userFiltersToSearchQuery(filters []string, username string) []string {
+ searchQueries := make([]string, len(filters))
+ for i, filter := range filters {
+ searchQueries[i] = "(" + filter + "=" + username + ")"
}
- return searchQuery
+ return searchQueries
}
func mapLDAPEntryToUser(
diff --git a/internal/idp/providers/ldap/session_test.go b/internal/idp/providers/ldap/session_test.go
index 69ba3a3256..89fee68718 100644
--- a/internal/idp/providers/ldap/session_test.go
+++ b/internal/idp/providers/ldap/session_test.go
@@ -49,31 +49,31 @@ func TestProvider_userFiltersToSearchQuery(t *testing.T) {
name string
fields []string
username string
- want string
+ want []string
}{
{
name: "zero",
fields: []string{},
username: "user",
- want: "",
+ want: []string{},
},
{
name: "one",
fields: []string{"test"},
username: "user",
- want: "(test=user)",
+ want: []string{"(test=user)"},
},
{
name: "three",
fields: []string{"test1", "test2", "test3"},
username: "user",
- want: "(test1=user)(test2=user)(test3=user)",
+ want: []string{"(test1=user)", "(test2=user)", "(test3=user)"},
},
{
name: "five",
fields: []string{"test1", "test2", "test3", "test4", "test5"},
username: "user",
- want: "(test1=user)(test2=user)(test3=user)(test4=user)(test5=user)",
+ want: []string{"(test1=user)", "(test2=user)", "(test3=user)", "(test4=user)", "(test5=user)"},
},
}
for _, tt := range tests {
From 4480cfcf56825b96afe3650ad6ba1976f9f5212b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tim=20M=C3=B6hlmann?=
Date: Fri, 16 May 2025 10:41:35 +0200
Subject: [PATCH 051/181] docs(advisory): position precision fix (#9882)
# Which Problems Are Solved
We are deploying precision fixes on the `position` values of the
eventstore. The fix itself might break systems that were already
affected by the bug.
# How the Problems Are Solved
Add a technical advisory that explains background and steps to fix the
Zitadel database when affected.
# Additional Context
- Original issue: [8671](https://github.com/zitadel/zitadel/issues/8671)
- Follow-up issue:
[8863](https://github.com/zitadel/zitadel/issues/8863)
- Re-fix: https://github.com/zitadel/zitadel/pull/9881
---
docs/docs/support/advisory/a10016.md | 98 ++++++++++++++++++++++++
docs/docs/support/technical_advisory.mdx | 12 +++
2 files changed, 110 insertions(+)
create mode 100644 docs/docs/support/advisory/a10016.md
diff --git a/docs/docs/support/advisory/a10016.md b/docs/docs/support/advisory/a10016.md
new file mode 100644
index 0000000000..794c354e42
--- /dev/null
+++ b/docs/docs/support/advisory/a10016.md
@@ -0,0 +1,98 @@
+---
+title: Technical Advisory 10016
+---
+
+## Date
+
+Versions:[^1]
+
+- v2.65.x: > v2.65.9
+
+Date: 2025-05-14
+
+Last updated: 2025-05-14
+
+[^1]: The mentioned fix is being rolled out gradually on multiple patch releases of Zitadel. This advisory will be updated as we release these versions.
+
+## Description
+
+### Background
+
+Zitadel uses a eventstore table as main source of truth for state changes.
+Projections are tables which provide alternative views of state, which are built using events.
+In order to know which events are reduced into projections, we use a `position` column in the eventstore and a dedicated table which records the current state.
+
+### Problem
+
+Zitadel prior to the listed version had a precision bug. The `position` column uses a fixed-point numeric type. In Zitadel's Go code we used a `float64`. In certain cases we noticed a precision loss when Zitadel updated the `current_states` table.
+
+## Impact
+
+During a past attempt to fix this, we got reports of failing projections inside Zitadel. Because the precision became exact certain compare operations like *equal*, *less then*, etc would now return different results. This was because the values in `current_states` would already have lost precision from a broken version. This might happen to **some** deployments or projections: there is only a small probability.
+
+We are releasing the fix again and your system might get affected.
+
+- Original issue: [8671](https://github.com/zitadel/zitadel/issues/8671)
+- Follow-up issue: [8863](https://github.com/zitadel/zitadel/issues/8863)
+
+## Mitigation
+
+When **after** deploying a fixed version and only when experiencing problems described by issue [8863](https://github.com/zitadel/zitadel/issues/8863), the following queries can be executed to fix `current_state` rows which have "broken" values. We recommend doing this in a transaction in order to double-check the affected rows, before committing the update.
+
+```sql
+begin;
+
+with
+ broken as (
+ select
+ s.projection_name,
+ s.instance_id,
+ s.aggregate_id,
+ s.aggregate_type,
+ s.sequence,
+ s."position" as old_position,
+ e."position" as new_position
+ from
+ projections.current_states s
+ join eventstore.events2 e on s.instance_id = e.instance_id
+ and s.aggregate_id = e.aggregate_id
+ and s.aggregate_type = e.aggregate_type
+ and s.sequence = e.sequence
+ and s."position" != e."position"
+ where
+ s."position" != 0
+ and projection_name != 'projections.execution_handler'
+ ),fixed as (
+ update projections.current_states s
+ set
+ "position" = b.new_position
+ from
+ broken b
+ where
+ s.instance_id = b.instance_id
+ and s.projection_name = b.projection_name
+ and s.aggregate_id = b.aggregate_id
+ and s.aggregate_type = b.aggregate_type
+ and s.sequence = b.sequence
+ )
+select
+ b.projection_name,
+ b.instance_id,
+ b.aggregate_id,
+ b.aggregate_type,
+ b.sequence,
+ b.old_position,
+ b.new_position
+from
+ broken b;
+```
+
+If the output from the above looks reasonable, for example not a huge difference between `old_position` and `new_position`, commit the transaction:
+
+```sql
+commit;
+```
+
+When there are no rows returned, your system was not affected by precision loss.
+
+When there's unexpected output, use `rollback;` instead.
diff --git a/docs/docs/support/technical_advisory.mdx b/docs/docs/support/technical_advisory.mdx
index 0d8818c32c..5d13cfe3e5 100644
--- a/docs/docs/support/technical_advisory.mdx
+++ b/docs/docs/support/technical_advisory.mdx
@@ -238,6 +238,18 @@ We understand that these advisories may include breaking changes, and we aim to
3.0.0
2025-03-31
+
+
+ A-10016
+
+ Position precision fix
+ Manual Intervention
+
+
+
+ 2.65.10
+ 2025-05-14
+